Add extesion engine gradebook ui in under the mastersGradebook route

This commit is contained in:
Rick Reilly
2018-10-18 14:21:35 -04:00
committed by Simon Chen
parent f43aa07aaa
commit 0c8d66bb2a
13 changed files with 1061 additions and 1 deletions

1
.gitignore vendored
View File

@@ -101,6 +101,7 @@ lms/static/certificates/css/
cms/static/css/
common/static/common/js/vendor/
common/static/common/css/vendor/
common/static/common/media/
common/static/bundles
webpack-stats.json

View File

@@ -0,0 +1,48 @@
<div class="grade-override-modal modal">
<div class="modal-content">
<div class="modal-body">
<p>
<%- strLib.heading %>
<span class="assignment-name-placeholder"></span>
</p>
<p class="block-id-placeholder"></p>
</div>
<div class="grade-override-message" style="display: none;"></div>
<div id="manual-grade-visibility" class="grade-visibility">
<input id="grades-published" name="grades-published" type="checkbox">
<label for="grades-published"><%- strLib.publishGrades %></label>
</div>
<div id="modal-table-empty-message">
<%- strLib.noMatch %>
</div>
<div class="grade-override-table-wrapper">
<table id="grade-override-modal-table">
<thead>
<th><%- strLib.studentNameHeading %></th>
<th id="auto-grade-header"></th>
<th id="adjusted-grade-header"></th>
<th><%- strLib.commentHeading %></th>
</thead>
<tbody>
<% _.each(studentsData, function(student){ %>
<tr data-user-id="<%- student.user_id %>" data-course-id="<%- student.course_id %>" data-username="<%- student.username %>">
<td class="user-student-name"><%- student.full_name || student.username %></td>
<td class="user-auto-grade" data-username="<%- student.username %>">
</td>
<td class="user-adjusted-grade" data-username="<%- student.username %>">
<input type=text>/<span></span>
</td>
<td class="user-grade-comment" data-username="<%- student.username %>">
<textarea></textarea>
</td>
</tr>
<% }) %>
</tbody>
</table>
</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>
</div>
</div>
</div>

View File

@@ -0,0 +1,80 @@
<table id="student-grades-table" class="display">
<thead>
<tr>
<th class="user-data"><%- strLib.userHeading %></th>
<% var sections = studentsData[0].section_breakdown; %>
<% _.each(sections, function(section, i){ %>
<% var tooltip = section.detail; %>
<% //The next two if statements are going to parse the section details for tooltip %>
<% if (_.str.include(tooltip, '=')) { %>
<% tooltip = tooltip.substring(0, tooltip.indexOf('=')); %>
<% } %>
<% if (_.str.include(tooltip, '-')) { %>
<% tooltip = tooltip.substring(0, tooltip.indexOf('-')); %>
<% } %>
<% var category = (section.category || '').replace(/[\W_]+/g, ''); %>
<% var chapterName = (section.chapter_name || '').replace(/[\W_]+/g, ''); %>
<%
var moduleId = '';
studentsData.every(function(student) {
moduleId = student.section_breakdown[i].module_id;
return _.contains(['', 'None'], moduleId);
});
%>
<th title="<%- tooltip %>" class="<%- category %> <%- chapterName %>">
<div class="assignment-label automatic-grade-label"><%- section.label %></div>
<% if (!(section.is_average || section.is_ag)) { %>
<i class="fa fa-pencil-square-o fa-2x grade-override" aria-hidden="true" data-block-id='<%- moduleId %>' data-assignment-name='<%- section.subsection_name %>' data-manual-grading='<%- section.is_manually_graded %>' data-published='<%- section.are_grades_published %>' data-section-block-id="<%- section.section_block_id %>"></i>
<% } %>
</th>
<% }) %>
<th title="Current grade"><div class="assignment-label"><%- strLib.userHeading %></div></th>
<th title="Total"><div class="assignment-label"><%- strLib.total %></div></th>
</tr>
</thead>
<tbody>
<% _.each(studentsData, function(student){ %>
<tr>
<td class="user-data"><a href="<%- student.progress_page_url %>"><%- student.full_name || student.username %></a></td>
<% _.each(student.section_breakdown, function(section){ %>
<% var category = (section.category || '').replace(/[\W_]+/g, ''); %>
<% var chapterName = (section.chapter_name || '').replace(/[\W_]+/g, ''); %>
<td class="grade_<%- section.letter_grade || 'none' %> data-score-container-class <%- category %> <%- chapterName %>"
title="<%- section.detail || '' %>"
data-block-id="<%- section.module_id || '' %>"
data-course-id="<%- student.course_id %>"
data-is-manually-graded="<%- section.is_manually_graded || '' %>"
data-percent="<%- section.percent || 0.0 %>"
data-score-absolute="<%- section.grade_description || '' %>"
data-score-auto="<%- section.auto_grade %>"
data-score-earned="<%- section.score_earned || 0 %>"
data-score-percent="<%- section.displayed_value || '' %>"
data-score-possible="<%- section.score_possible || 0 %>"
data-sort="<%- parseInt(section.score_earned) %>"
data-student-id="<%- student.user_id %>">
<%- (section.grade_description || '') %>
</td>
<% }) %>
<td class="grade_<%- student.current_letter_grade %> data-score-container-class"
title="Current grade">
<%- (student.current_percent * 100).toFixed(2) %>&percnt;
</td>
<td class="grade_<%- student.total_letter_grade %> data-score-container-class"
title="Total">
<%- (student.percent * 100).toFixed(2) %>&percnt;
</td>
</tr>
<% }) %>
</tbody>
</table>
<div id="grade-override-modal"></div>

View File

@@ -0,0 +1,4 @@
<% _.each(gradingPolicies, function(policy){ %>
<% var value = policy.replace(/[\W_]+/g, ''); %>
<option value="<%- value %>"><%- policy %></option>
<% }) %>

View File

@@ -0,0 +1 @@
table.DTFC_Cloned thead,table.DTFC_Cloned tfoot{background-color:white}div.DTFC_Blocker{background-color:white}div.DTFC_LeftWrapper table.dataTable,div.DTFC_RightWrapper table.dataTable{margin-bottom:0;z-index:2}div.DTFC_LeftWrapper table.dataTable.no-footer,div.DTFC_RightWrapper table.dataTable.no-footer{border-bottom:none}

View File

@@ -4,7 +4,7 @@ Instructor API endpoint urls.
from django.conf.urls import url
from lms.djangoapps.instructor.views import api, gradebook_api
from lms.djangoapps.instructor.views import api, gradebook_api, writable_gradebook_api
urlpatterns = [
url(r'^students_update_enrollment$', api.students_update_enrollment, name='students_update_enrollment'),
@@ -73,6 +73,7 @@ urlpatterns = [
# spoc gradebook
url(r'^gradebook$', gradebook_api.spoc_gradebook, name='spoc_gradebook'),
url(r'^writable_gradebook$', writable_gradebook_api.writable_gradebook, name='writable_gradebook'),
url(r'^gradebook/(?P<offset>[0-9]+)$', gradebook_api.spoc_gradebook, name='spoc_gradebook'),

View File

@@ -0,0 +1,49 @@
"""
Grade book view for instructor and pagination work (for grade book)
which is currently use by ccx and instructor apps.
"""
from django.db import transaction
from django.http import HttpResponseNotFound
from django.views.decorators.cache import cache_control
from opaque_keys.edx.keys import CourseKey
from edxmako.shortcuts import render_to_response
from lms.djangoapps.courseware.courses import get_course_with_access
from lms.djangoapps.grades.config.waffle import waffle_flags, WRITABLE_GRADEBOOK
from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory
from lms.djangoapps.instructor.views.api import require_level
@transaction.non_atomic_requests
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
@require_level('staff')
def writable_gradebook(request, course_id):
"""
Show the writable gradebook for this course:
- Only displayed to course staff
"""
course_key = CourseKey.from_string(course_id)
if not waffle_flags()[WRITABLE_GRADEBOOK].is_enabled(course_key):
return HttpResponseNotFound()
course = get_course_with_access(request.user, 'load', course_key)
course_grade = CourseGradeFactory().read(request.user, course)
courseware_summary = course_grade.chapter_grades.values()
course_sections = []
for chapter in courseware_summary:
chapter_name = chapter['display_name']
for section in chapter['sections']:
if section.problem_scores and section.graded and chapter_name not in course_sections:
course_sections.append(chapter_name)
return render_to_response('courseware/writable_gradebook.html', {
'number_of_students': 2,
'course': course,
'course_id': course_key,
'course_sections': course_sections,
# Checked above
'staff_access': True,
'ordered_grades': sorted(course.grade_cutoffs.items(), key=lambda i: i[1], reverse=True),
})

View File

@@ -0,0 +1,110 @@
var Gradebook = function($element, $gradeTableWrapper) {
"use strict";
var $body = $('body');
var $grades = $element.find('#gradebook-table-container');
var $gradeTable = $gradeTableWrapper.find('#student-grades-table');
var $search = $element.find('.student-search-field');
var $leftShadow = $('<div class="left-shadow"></div>');
var $rightShadow = $('<div class="right-shadow"></div>');
var tableHeight = $gradeTable.height();
var maxScroll = $gradeTable.width() - $grades.width();
var mouseOrigin;
var tableOrigin;
var startDrag = function(e) {
mouseOrigin = e.pageX;
tableOrigin = $gradeTable.position().left;
$body.addClass('no-select');
$body.bind('mousemove', onDragTable);
$body.bind('mouseup', stopDrag);
};
/**
* - Called when the user drags the gradetable
* - Calculates targetLeft, which is the desired position
* of the grade table relative to its leftmost position, using:
* - the new x position of the user's mouse pointer;
* - the gradebook's current x position, and;
* - the value of maxScroll (gradetable width - container width).
* - Updates the position and appearance of the gradetable.
*/
var onDragTable = function(e) {
var offset = e.pageX - mouseOrigin;
var targetLeft = clamp(tableOrigin + offset, maxScroll, 0);
updateHorizontalPosition(targetLeft);
setShadows(targetLeft);
};
var stopDrag = function() {
$body.removeClass('no-select');
$body.unbind('mousemove', onDragTable);
$body.unbind('mouseup', stopDrag);
};
var setShadows = function(left) {
var padding = 30;
var leftPercent = clamp(-left / padding, 0, 1);
$leftShadow.css('opacity', leftPercent);
var rightPercent = clamp((maxScroll + left) / padding, 0, 1);
$rightShadow.css('opacity', rightPercent);
};
var clamp = function(val, min, max) {
if(val > max) { return max; }
if(val < min) { return min; }
return val;
};
/**
* - Called when the browser window is resized.
* - Recalculates maxScroll (gradetable width - container width).
* - Calculates targetLeft, which is the desired position
* of the grade table relative to its leftmost position, using:
* - the gradebook's current x position, and:
* - the new value of maxScroll
* - Updates the position and appearance of the gradetable.
*/
var onResizeTable = function() {
maxScroll = $gradeTable.width() - $grades.width();
var targetLeft = clamp($gradeTable.position().left, maxScroll, 0);
updateHorizontalPosition(targetLeft);
setShadows(targetLeft);
};
/**
* - Called on table drag and on window (table) resize.
* - Takes a integer value for the desired (pixel) offset from the left
* (zero/origin) position of the grade table.
* - Uses that value to position the table relative to its leftmost
* possible position within its container.
*
* @param {Number} left - The desired pixel offset from left of the
* desired position. If the value is 0, the gradebook should be moved
* all the way to the left side relative to its parent container.
*/
var updateHorizontalPosition = function(left) {
$grades.scrollLeft(left);
};
var highlightRow = function() {
$element.find('.highlight').removeClass('highlight');
var index = $(this).index();
$gradeTable.find('tr').eq(index + 1).addClass('highlight');
};
var filter = function() {
var term = $(this).val();
if(term.length > 0) {
$gradeTable.find('tbody tr').hide();
} else {
$gradeTable.find('tbody tr').show();
}
};
};

View File

@@ -0,0 +1,638 @@
function _templateLoader(templateName, staticPath, callback, errorCallback) {
var templateURL = staticPath + '/common/templates/gradebook/' + templateName + '.underscore';
$.ajax({
url: templateURL,
method: 'GET',
dataType: 'html',
success: function (data) {
callback(data);
},
error: function (errorMessage) {
console.log(errorMessage);
errorCallback('Error has occurred while rendering table.');
}
});
}
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;
$.ajax({
url: postUrl,
method: 'POST',
contentType: 'application/json; charset=utf-8',
dataType: 'json',
data: JSON.stringify(cleanData),
success: function (data) {
callback(data);
},
error: function (errorMessage) {
console.log(errorMessage);
errorCallback('Error has occurred while updating grades.');
}
});
}
function getEdxUserInfoAsObject() {
return JSON.parse($.cookie('edx-user-info').replace(/\\054/g, ',').replace(/^"(.*)"$/, '$1').replace(/\\"/g, '"'));
}
$(document).ready(function() {
var dataTable,
$gradebookWrapper = $('.gradebook-content'),
$courseSectionFilter = $gradebookWrapper.find('#course-sections'),
$errorMessageContainer = $gradebookWrapper.find('#error-message'),
$filtersWrapper = $gradebookWrapper.find('#filters-container'),
$gradebookNotification = $gradebookWrapper.find('#gradebook-notification'),
$gradesTableWrapper = $gradebookWrapper.find('#gradebook-table-container'),
$gradingPolicyFilter = $filtersWrapper.find('#grading-policy'),
adjustedGradesData = {},
courseID = $gradebookWrapper.attr('data-course-id'),
edxUserInfo = getEdxUserInfoAsObject(),
gradeBookData = [],
gradeOverrideObject = {},
isFetchingComplete = false,
isFetchingSuccessful = true,
isManualGrading = false,
modalDataTable,
module_list = {'users': {}},
renderAllGradebook = true,
sectionBlockId = '',
staticPath = $gradebookWrapper.attr('data-static-path'),
userAdjustedGrades = {},
userAutoGrades = {},
userComments = {},
createMainDataTable = function(studentsDataLength) {
const $gradebookErrorMessageContainer = $gradebookWrapper.find('#gradebook-table-empty-message');
const $studentGradesTable = $gradesTableWrapper.find('#student-grades-table');
const options = {
fixedColumns: true,
language: {
zeroRecords: ''
},
paging: studentsDataLength > 10,
scrollX: true
};
dataTable = initializeDataTable($studentGradesTable, options, studentsDataLength);
setUpDataTableSearch($studentGradesTable, $gradebookErrorMessageContainer);
$studentGradesTable.on('draw.dt', displayGrades);
},
createModalTable = function(studentsDataLength) {
const $gradeOverrideModalTable = $gradesTableWrapper.find('#grade-override-modal-table');
const $modalErrorMessageContainer = $gradesTableWrapper.find('#modal-table-empty-message');
const options = {
columnDefs: [{
orderable: false,
targets: 3
}],
language: {
zeroRecords: ''
},
paging: studentsDataLength > 10
};
modalDataTable = initializeDataTable($gradeOverrideModalTable, options, studentsDataLength);
setUpDataTableSearch($gradeOverrideModalTable, $modalErrorMessageContainer);
},
destroyDataTable = function($table) {
if ($.fn.DataTable.isDataTable($table)) {
$table.DataTable().destroy();
$table.unbind();
}
},
displayError = function(message) {
$errorMessageContainer.text(message);
$errorMessageContainer.toggleClass('hidden');
},
displayAbsoluteGrade = function($cell) {
var $input = $cell.find('input'),
title = $cell.attr('title');
if (title !== 'Total' && title !== 'Current grade' && $input.length) {
$input.prop('disabled', false);
$input.val($cell.attr('data-score-earned'));
return;
}
$cell.text($cell.attr('data-score-absolute'));
},
displayGrades = function() {
var display = $('#table-data-view-percent').is(':checked') ? displayPercentGrade : displayAbsoluteGrade;
$('#save-grade-field').hide();
$('.data-score-container-class').each(function() {
display($(this));
});
},
displayPercentGrade = function($cell) {
var $input = $cell.find('input'),
title = $cell.attr('title');
if (title !== 'Total' && $input.length){
$input.prop('disabled', true);
$input.val($cell.attr('data-score-percent'));
return;
}
$cell.text($cell.attr('data-score-percent'));
},
fetchGrades = function(get_url) {
$.ajax({
type: 'GET',
url: get_url,
contentType: 'application/json; charset=utf-8',
success: onPageFetched,
failure: function(errMsg) {
isFetchingComplete = true;
isFetchingSuccessful = false;
console.log(errMsg);
displayError('Error has occurred while fetching grades.');
}
});
},
filterGradebook = function() {
var gradingPolicy = $gradingPolicyFilter.val(),
courseSection = $courseSectionFilter.val(),
filterClasses = '.user-data';
if (gradingPolicy && courseSection)
filterClasses += ',.' + gradingPolicy + '.' + courseSection;
else if (gradingPolicy || courseSection)
filterClasses += ',.' + (gradingPolicy || courseSection);
else
filterClasses = '';
dataTable.columns(':not(' + filterClasses + ')').visible(false, false);
dataTable.columns(filterClasses).visible(true, false);
dataTable.columns.adjust().draw(false);
},
initializeDataTable = function($table, options, studentsDataLength) {
$table.on('length.dt', function(_, _, tableLength) {
// If the provided data is longer than the table length selected
// display the paggination buttons, otherwise hide them.
$(this).parents('.dataTables_wrapper')
.find('.dataTables_paginate')
.toggleClass('hidden', studentsDataLength <= tableLength);
});
return $table.DataTable(options);
},
onFinishedFetchingGrades = function(response) {
isFetchingComplete = true;
isFetchingSuccessful = true;
if (renderAllGradebook)
$filtersWrapper.toggleClass('hidden');
$gradebookNotification.toggleClass('hidden');
gradeBookData = gradeBookData.concat(response.results);
gradeBookData = gradeBookData.map(data => {
data.section_breakdown = data.section_breakdown.filter(b => b.chapter_name !== 'holding section')
return data;
});
renderGradebook(gradeBookData);
},
onPageFetched = function(response) {
if (response.next) {
gradeBookData = gradeBookData.concat(response.results);
return fetchGrades(response.next);
}
onFinishedFetchingGrades(response);
},
renderGradingPolicyFilters = function(studentsData) {
_templateLoader('_grading_policies', staticPath, function(template) {
var $tpl = edx.HtmlUtils.template(template)({
gradingPolicies: Object.keys(studentsData[0].aggregates)
}).toString();
$('#grading-policy').append($tpl);
$('#grading-policy').append(edx.HtmlUtils.ensureHtml(displayError).toString());
}, displayError);
},
renderGradebook = function(studentsData) {
if (renderAllGradebook)
renderGradingPolicyFilters(studentsData);
renderGradebookTable(studentsData);
},
renderGradebookTable = function(studentsData) {
_templateLoader('_gradebook_table', staticPath, function(template) {
var $tpl = edx.HtmlUtils.template(template)({
studentsData: studentsData,
strLib: {
userHeading: gettext('Username'),
currentGrade: gettext('Current grade'),
total: gettext('Total')
}
}).toString();
$gradesTableWrapper.append($tpl);
createMainDataTable(studentsData.length);
ShowBlockIdEventBinder();
filterGradebook();
}, displayError);
renderAllGradebook = true;
},
startFetchingGrades = function() {
$gradebookNotification.toggleClass('hidden');
fetchGrades('api/grades/v1/gradebook/' + courseID + '/');
};
$gradingPolicyFilter.change(function() { filterGradebook(); });
$courseSectionFilter.change(function() { filterGradebook(); });
function renderModalTemplateData(template) {
var blockID = $(gradeOverrideObject).attr('data-block-id');
var studentsData = [];
var tpl = edx.HtmlUtils.template(template);
gradeBookData.map(function(userData){
var gradeData = userData.section_breakdown.filter(function(sectionData){
return (sectionData.module_id === blockID);
});
if (!_.isEmpty(gradeData)) {
var auto_grade = parseFloat(gradeData[0].auto_grade);
var score_earned = parseFloat(gradeData[0].score_earned);
var score_possible = parseFloat(gradeData[0].score_possible);
var username = userData.username;
userComments[username] = gradeData[0].comment;
if (! (isNaN(score_earned) || isNaN(score_possible))) {
if (! isNaN(auto_grade)) {
userAutoGrades[username] = auto_grade + '/' + score_possible;
userAdjustedGrades[username] = score_earned + '/' + score_possible;
}
else
userAutoGrades[username] = score_earned + '/' + score_possible;
studentsData.push(userData);
}
}
});
edx.HtmlUtils.setHtml(
$('#grade-override-modal'),
tpl({
studentsData: studentsData,
strLib: {
heading: gettext("The Assignment name is:"),
publishGrades: gettext("Publish grades"),
noMatch: gettext("No matching records found"),
studentNameHeading: gettext("Student Name"),
commentHeading: gettext("Comment"),
save: gettext("Save"),
cancel: gettext("Cancel")
}
})
);
createModalTable(studentsData.length);
fillModalTemplate();
}
function fillModalTemplate() {
var $modal = $('.grade-override-modal');
var $adjustedGradeHeader = $modal.find('#adjusted-grade-header');
var $autoGradeHeader = $modal.find('#auto-grade-header');
var $manualGradeVisibilityWrapper = $modal.find('#manual-grade-visibility');
var $saveGradeOverrideButton = $modal.find('.grade-override-modal-save');
var $tableWrapper = $modal.find('.grade-override-table-wrapper');
var assignmentName = $(gradeOverrideObject).attr('data-assignment-name');
var blockID = $(gradeOverrideObject).attr('data-block-id');
var dataPublished = $(gradeOverrideObject).attr('data-published') || false;
sectionBlockId = $(gradeOverrideObject).attr('data-section-block-id');
gradesPublished = JSON.parse(dataPublished);
isManualGrading = JSON.parse($(gradeOverrideObject).attr('data-manual-grading'));
$modal.find('.assignment-name-placeholder').text(assignmentName);
$modal.find('.block-id-placeholder').text(blockID);
if ( _.isEmpty(userAutoGrades) ) {
$tableWrapper.hide();
$manualGradeVisibilityWrapper.toggle(false);
$modal.find('.grade-override-message').text(gettext('There are no student grades to adjust.'));
$modal.find('.grade-override-message').show();
$saveGradeOverrideButton.hide();
}
else {
$adjustedGradeHeader.text(isManualGrading ? 'Manual grade' : 'Adjusted grade');
$autoGradeHeader.text(isManualGrading ? 'Current grade' : 'Auto grade');
$manualGradeVisibilityWrapper.toggle(isManualGrading);
$saveGradeOverrideButton.attr('data-manual-grading', isManualGrading);
$manualGradeVisibilityWrapper.attr('data-visibility', gradesPublished);
$('input[name=grades-published]').prop('checked', gradesPublished);
$tableWrapper.attr('data-manual-grading', isManualGrading);
$tableWrapper.show();
$saveGradeOverrideButton.show().prop('disabled', true);
modalDataTable.$('tr').each(function(){
$(this).attr('data-block-id', blockID);
var $adjustedGradePlaceholder = $(this).find('td.user-adjusted-grade');
var $autoGradePlaceholder = $(this).find('td.user-auto-grade');
var $commentPlaceholder = $(this).find('td.user-grade-comment');
var $commentTextArea = $commentPlaceholder.find('textarea');
var comment;
var username = $autoGradePlaceholder.attr('data-username');
if (username in userAutoGrades) {
$autoGradePlaceholder.text(userAutoGrades[username]);
var autoEarnedGrade = userAutoGrades[username].split('/')[0],
autoPossibleGrade = userAutoGrades[username].split('/')[1];
$adjustedGradePlaceholder.attr('data-score-earned', autoEarnedGrade);
$adjustedGradePlaceholder.attr('data-score-possible', autoPossibleGrade);
$autoGradePlaceholder.attr('data-sort', autoEarnedGrade);
if (username in userAdjustedGrades) {
var adjustedGrade = userAdjustedGrades[username].split('/')[0];
$adjustedGradePlaceholder.attr('data-score-earned', adjustedGrade);
$adjustedGradePlaceholder.attr('data-sort', adjustedGrade);
$adjustedGradePlaceholder.addClass('has-adjusted-score');
if (autoEarnedGrade != adjustedGrade){
DisplayGradeComment(username, $commentPlaceholder, $commentTextArea);
}
else
$commentTextArea.prop('disabled', true).val('');
}
else if (isManualGrading) {
$adjustedGradePlaceholder.attr('data-sort', autoEarnedGrade);
DisplayGradeComment(username, $commentPlaceholder, $commentTextArea);
}
else {
$(this).find('.user-grade-comment textarea').attr('disabled', 'disabled');
$adjustedGradePlaceholder.attr('data-sort', autoEarnedGrade);
$commentTextArea.prop('disabled', true).val('');
}
$adjustedGradePlaceholder.find('input').val($adjustedGradePlaceholder.attr('data-score-earned'));
$adjustedGradePlaceholder.find('span').text($adjustedGradePlaceholder.attr('data-score-possible'));
}
else
$(this).hide();
});
}
$modal.show();
}
/* Autograde override modal window manipulation */
$(document).on('click', '.grade-override', function() {
gradeOverrideObject = this;
_templateLoader('_gradebook_modal_table', staticPath, renderModalTemplateData, displayError);
});
function setUpDataTableSearch($table, $tableEmptyMessage) {
$table.on('search.dt', function () {
if (!$table.DataTable().page.info().recordsDisplay) {
$tableEmptyMessage.show();
}
else {
$tableEmptyMessage.hide();
}
});
}
$(document).on('click', '.grade-override-modal-close', function(){
gradebookOverrideModalReset();
});
function gradebookOverrideModalReset() {
var $modal = $('.grade-override-modal');
adjustedGradesData = {};
userAdjustedGrades = {};
userAutoGrades = {};
userComments = {};
$modal.hide();
$modal.find('.grade-override-table-wrapper').find('tr').show();
$modal.find('#manual-grade-visibility').hide();
$modal.find('.grade-override-message').removeClass('error').empty().hide();
$modal.find('table').find('input').removeClass('score-visited').removeClass('error');
$modal.find('table').find('textarea').removeClass('score-visited').removeClass('error');
$modal.find('#modal-table-empty-message').hide();
destroyDataTable($('#grade-override-modal-table'));
}
function DisplayGradeComment(username, $commentPlaceholder, $commentTextArea) {
comment = userComments[username];
$commentPlaceholder.attr('data-comment', comment);
$commentTextArea.prop('disabled', false).val(comment);
}
/* Block ID modal window manipulation */
function ShowBlockIdEventBinder() {
$('.eye-icon.block-id-info').on('click', function(e){
e.stopPropagation();
$('.block-id-modal').find('.block-id-placeholder').empty();
$('.block-id-modal').find('.block-id-placeholder').text($(this).data('block-id'));
$('.block-id-modal').find('.display-name-placeholder').text($(this).data('display-name'));
$('.block-id-modal').show();
});
}
function HasUserMadeChanges() {
var areScoresModified = $('.score-visited').length > 0;
var originalGradeVisibility = $('#manual-grade-visibility').attr('data-visibility');
var currentGradeVisibility = JSON.stringify($('input[name=grades-published]').prop('checked'));
return areScoresModified || originalGradeVisibility !== currentGradeVisibility;
}
function ToggleSaveButton(shouldDisable) {
var $modalSaveButton = $('.grade-override-modal').find('.grade-override-modal-save');
$modalSaveButton.prop('disabled', shouldDisable);
}
$(document).on('keyup focus', '.user-adjusted-grade input', function(){
var $row = $(this).parents('tr'),
$cell = $(this).parents('td'),
$commentTextArea = $row.find('.user-grade-comment textarea'),
autoGrade = $row.find('.user-auto-grade').html().split('/')[0],
previousGrade = $cell.attr('data-score-earned');
adjustedGrade = $(this).val();
$cell.attr('data-sort', adjustedGrade);
if (autoGrade != adjustedGrade || previousGrade != adjustedGrade)
$(this).addClass('score-visited');
else
$(this).removeClass('score-visited');
ToggleSaveButton(!HasUserMadeChanges());
if (!isManualGrading) {
if (autoGrade == adjustedGrade)
$commentTextArea.prop('disabled', true).val('');
else
$commentTextArea.prop('disabled', false);
}
modalDataTable.rows().invalidate();
});
$(document).on('keyup focus', '.user-grade-comment textarea', function(){
var originalComment = $(this).parents('td').attr('data-comment'),
changedComment = $(this).val();
$(this).toggleClass('score-visited', changedComment != originalComment)
ToggleSaveButton(!HasUserMadeChanges());
});
$(document).on('change', 'input[name=grades-published]', function() {
ToggleSaveButton(!HasUserMadeChanges());
});
function collectOverrideGradebookData() {
var $modal = $('.grade-override-modal');
var $table = $modal.find('table').dataTable();
$table.$('tr').each(function(){
var $row = $(this);
var $gradeCell = $row.find('.user-adjusted-grade');
var $grade = $gradeCell.find('input');
var $commentCell = $row.find('.user-grade-comment');
var $comment = $commentCell.find('textarea');
var username = $gradeCell.attr('data-username');
var autoGrade;
var grade;
var removeAdjustedGrade;
if ($grade.hasClass('score-visited') || $comment.hasClass('score-visited'))
adjustedGradesData[username] = {
'block_id' : $row.attr('data-block-id'),
'max_grade' : $gradeCell.attr('data-score-possible'),
'state' : { 'username': edxUserInfo.username},
'user_id' : $row.attr('data-user-id')
};
if (username in adjustedGradesData) {
autoGrade = $row.find('.user-auto-grade').text().split('/')[0];
grade = $grade.val().trim();
removeAdjustedGrade = isManualGrading || autoGrade === grade;
adjustedGradesData[username].grade = grade;
adjustedGradesData[username].state.comment = $comment.val().trim();
adjustedGradesData[username].remove_adjusted_grade = removeAdjustedGrade;
adjustedGradesData[username].section_block_id = sectionBlockId;
}
});
}
$(document).on('click', '.grade-override-modal-save', function() {
var visibilityData = {};
if (isManualGrading) {
visibilityData = {
'block_id': $('.block-id-placeholder').html(),
'visibility': JSON.stringify($('input[name=grades-published]').prop('checked')),
}
}
collectOverrideGradebookData();
if (Object.keys(adjustedGradesData).length === 0 && !isManualGrading)
return;
var validStatus = ValidateAdjustedGradesData();
if (validStatus) {
courseXblockUpdater(
courseID,
adjustedGradesData,
visibilityData,
function(data){
gradebookOverrideModalReset();
renderAllGradebook = false;
gradeBookData = [];
$gradesTableWrapper.empty();
startFetchingGrades();
}, function(data){
console.log(data);
}
);
}
});
function ValidateAdjustedGradesData() {
var isValid = true;
var $table = $('.grade-override-modal').find('table');
var $messageField = $('.grade-override-modal').find('.grade-override-message');
$messageField.empty();
_.each(adjustedGradesData, function(data, username){
adjustedGradesData[username].errors = [];
var userAdjustedGradeSelector = '*[data-username="' + username + '"].user-adjusted-grade';
var $adjustedGradePlaceholder = $table.find(userAdjustedGradeSelector).find('input');
// Is it a valid number
if (isNaN(data.grade)) {
isValid = false;
$adjustedGradePlaceholder.addClass('error');
adjustedGradesData[username].errors.push('Adjusted grade must be an integer number');
}
// Is it within range
var floatGrade = parseFloat(data.grade);
var errorMessage;
if (floatGrade < 0 || floatGrade > parseFloat(data.max_grade)) {
errorMessage = 'Adjusted grade must be within range [0 - ' + data.max_grade + ']';
isValid = false;
$adjustedGradePlaceholder.addClass('error');
adjustedGradesData[username].errors.push(errorMessage);
}
for (var i = 0; i < adjustedGradesData[username].errors.length; i++) {
$errorMessage = edx.HtmlUtils.joinHtml('Error for user ', username, ': ', adjustedGradesData[username].errors[i], '<br>').toString();
$messageField.append($errorMessage);
}
if (adjustedGradesData[username].errors.length === 0) {
$adjustedGradePlaceholder.removeClass('error');
delete adjustedGradesData[username].errors;
}
});
if (! isValid) {
$messageField.addClass('error');
$messageField.show();
}
return isValid;
}
$(document).on('change', '#table-data-view-percent', displayGrades);
$(document).on('change', '#table-data-view-absolute', displayGrades);
$('.data-score-container-class').each(function(){
var title = $(this).attr('title');
if (title !== 'Total' && title !== 'Current grade')
if ($(this).find('input').length)
$(this).find('input').prop('disabled', false);
else
$(this).text($(this).attr('data-score-absolute'));
});
$(document).on('change', '#save-grade-field textarea', function(){
var editor = $('#save-grade-field'),
studentID = editor.attr('data-student-id'),
blockID = editor.attr('data-block-id'),
module_key = studentID + blockID;
if (!module_list.users[module_key])
module_list.users[module_key] = {
'user_id': studentID,
'grade': parseFloat(editor.attr('data-new-score')).toFixed(2),
'max_grade': parseFloat(editor.attr('data-score-possible')).toFixed(2),
'course_id': courseID,
'block_id': blockID,
'state': {}
};
module_list.users[module_key].state.comment = $(this).val();
});
if ($gradebookWrapper.attr('data-number-of-students') > 0)
startFetchingGrades();
});

View File

@@ -0,0 +1,92 @@
<%page expression_filter="h"/>
<%inherit file="/main.html" />
<%namespace name='static' file='/static_content.html'/>
<%!
import re
from django.utils.translation import ugettext as _
from django.core.urlresolvers import reverse
%>
<%block name="js_extra">
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.stack.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.symbol.js')}"></script>
<script type="text/javascript" src="${static.url('common/js/vendor/jquery.dataTables.js')}"></script>
<script type="text/javascript" src="${static.url('common/js/vendor/dataTables.fixedColumns.min.js')}"></script>
<script type="text/javascript" src="${static.url('js/jquery.writable_gradebook.js')}"></script>
<script type="text/javascript" src="${static.url('js/writable_gradebook.js')}"></script>
</%block>
<%block name="headextra">
<link rel="stylesheet" type="text/css" href="${static.url('common/media/vendor/media/css/jquery.dataTables.min.css')}">
<link rel="stylesheet" type="text/css" href="${static.url('css/vendor/fixedColumns.dataTables.min.css')}">
<%static:css group='style-course-vendor'/>
<%static:css group='style-course'/>
</%block>
<%include file="/courseware/course_navigation.html" args="active_page=''" />
<%def name="get_page_url(page_size)">
${page_url}?offset=${page['offset']}&pagesize=${page_size}
</%def>
<%def name="format_class(name)">${re.sub(r'[^a-zA-Z0-9]', '', name)}</%def>
<section class="container">
<div class="gradebook-wrapper">
<section id="gradebook-main-content" class="gradebook-content" data-course-id="${course_id.to_deprecated_string()}" data-number-of-students="${number_of_students}" data-static-path="/static" data-staff-username="${user}">
<h1>${_("Gradebook")}</h1>
<div id="error-message" class="error hidden"></div>
%if number_of_students > 0:
<div id="filters-container" class="hidden">
<div class="view-container">
<span>${_("Score View:")} </span>
<input type="radio" id="table-data-view-percent" name="table-data-view" value="percent"> ${_("Percent")}
<input type="radio" id="table-data-view-absolute" name="table-data-view" value="absolute" checked> ${_("Absolute")}
<a id="download-grade-report" href="../../instructor?report=grade-report#view-data_download">${_("Download Grade Report")}</a>
</div>
<div id="gp-filter" class="gradebook-filter">
<select id='grading-policy'>
<option value=''>${_("Grading Policy")}</option>
</select>
</div>
<div id="section-filter" class="gradebook-filter">
<select id="course-sections">
<option value="">${_("Section")}</option>
%for section in course_sections:
## [NDPD-729] Removing spaces from section name to get a string to be used as a CSS class in grade table.
<option value="${format_class(section)}">
${section}
</option>
%endfor
</select>
</div>
</div>
<div id="gradebook-notification" class="hidden">
<span>
${_("Fetching gradebook data")}
<span class="fa fa-spinner fa-pulse" aria-hidden="true"></span>
<div class="progress gradebook-progress-bar">
<progress class="progress-bar" role="progressbar" value=0 max=100>
</progress>
</div>
<span class="gradebook-progress-count"></span>
</span>
</div>
<div id="gradebook-table-empty-message">
${_("No matching records found")}
</div>
<div id="gradebook-table-container">
</div>
%else:
${_("There are no students enrolled in this course.")}
%endif
</section>
</div>
</section>

25
package-lock.json generated
View File

@@ -2925,6 +2925,31 @@
"whatwg-url": "6.5.0"
}
},
"datatables": {
"version": "1.10.18",
"resolved": "https://registry.npmjs.org/datatables/-/datatables-1.10.18.tgz",
"integrity": "sha512-ntatMgS9NN6UMpwbmO+QkYJuKlVeMA2Mi0Gu/QxyIh+dW7ZjLSDhPT2tWlzjpIWEkDYgieDzS9Nu7bdQCW0sbQ==",
"requires": {
"jquery": "2.2.4"
}
},
"datatables.net": {
"version": "1.10.19",
"resolved": "https://registry.npmjs.org/datatables.net/-/datatables.net-1.10.19.tgz",
"integrity": "sha512-+ljXcI6Pj3PTGy5pesp3E5Dr3x3AV45EZe0o1r0gKENN2gafBKXodVnk2ypKwl2tTmivjxbkiqoWnipTefyBTA==",
"requires": {
"jquery": "2.2.4"
}
},
"datatables.net-fixedcolumns": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/datatables.net-fixedcolumns/-/datatables.net-fixedcolumns-3.2.6.tgz",
"integrity": "sha512-PtEs2tllcHRVZj7fwmAQBWGJ5URRQZpDG2pJsh5jusvnRje3w1+KueMZm60iCtfOkIlUn+/j2+MghxLx/8yfKQ==",
"requires": {
"datatables.net": "1.10.19",
"jquery": "2.2.4"
}
},
"date-now": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz",

View File

@@ -30,6 +30,8 @@
"hls.js": "0.9.0",
"imports-loader": "0.7.1",
"jquery": "2.2.4",
"datatables": "1.10.18",
"datatables.net-fixedcolumns": "3.2.6",
"jquery-migrate": "1.4.1",
"jquery.scrollto": "2.1.2",
"js-cookie": "2.2.0",

View File

@@ -54,6 +54,9 @@ NPM_INSTALLED_LIBRARIES = [
'backbone.paginator/lib/backbone.paginator.js',
'backbone/backbone.js',
'bootstrap/dist/js/bootstrap.bundle.js',
'datatables/media',
'datatables.net/js/jquery.dataTables.js',
'datatables.net-fixedcolumns/js/dataTables.fixedColumns.min.js',
'hls.js/dist/hls.js',
'jquery-migrate/dist/jquery-migrate.js',
'jquery.scrollto/jquery.scrollTo.js',
@@ -79,6 +82,8 @@ NPM_INSTALLED_DEVELOPER_LIBRARIES = [
NPM_JS_VENDOR_DIRECTORY = path('common/static/common/js/vendor')
NPM_CSS_VENDOR_DIRECTORY = path("common/static/common/css/vendor")
NPM_CSS_DIRECTORY = path("common/static/common/css")
NPM_MEDIA_DIRECTORY = path("common/static/common/media")
NPM_MEDIA_VENDOR_DIRECTORY = path("common/static/common/media/vendor")
# system specific lookup path additions, add sass dirs if one system depends on the sass files for other systems
SASS_LOOKUP_DEPENDENCIES = {
@@ -601,6 +606,8 @@ def process_npm_assets():
if library.endswith('.css') or library.endswith('.css.map'):
vendor_dir = NPM_CSS_VENDOR_DIRECTORY
elif library.endswith('/media'):
vendor_dir = NPM_MEDIA_VENDOR_DIRECTORY
else:
vendor_dir = NPM_JS_VENDOR_DIRECTORY
if os.path.exists(library_path):
@@ -631,6 +638,8 @@ def process_npm_assets():
NPM_JS_VENDOR_DIRECTORY.mkdir_p()
NPM_CSS_DIRECTORY.mkdir_p()
NPM_CSS_VENDOR_DIRECTORY.mkdir_p()
NPM_MEDIA_DIRECTORY.mkdir_p()
NPM_MEDIA_VENDOR_DIRECTORY.mkdir_p()
# Copy each file to the vendor directory, overwriting any existing file.
print("Copying vendor files into static directory")