Add extesion engine gradebook ui in under the mastersGradebook route
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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) %>%
|
||||
</td>
|
||||
|
||||
<td class="grade_<%- student.total_letter_grade %> data-score-container-class"
|
||||
title="Total">
|
||||
<%- (student.percent * 100).toFixed(2) %>%
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div id="grade-override-modal"></div>
|
||||
@@ -0,0 +1,4 @@
|
||||
<% _.each(gradingPolicies, function(policy){ %>
|
||||
<% var value = policy.replace(/[\W_]+/g, ''); %>
|
||||
<option value="<%- value %>"><%- policy %></option>
|
||||
<% }) %>
|
||||
1
common/static/css/vendor/fixedColumns.dataTables.min.css
vendored
Normal file
1
common/static/css/vendor/fixedColumns.dataTables.min.css
vendored
Normal 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}
|
||||
@@ -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'),
|
||||
|
||||
|
||||
49
lms/djangoapps/instructor/views/writable_gradebook_api.py
Normal file
49
lms/djangoapps/instructor/views/writable_gradebook_api.py
Normal 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),
|
||||
})
|
||||
110
lms/static/js/jquery.writable_gradebook.js
Normal file
110
lms/static/js/jquery.writable_gradebook.js
Normal 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();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
|
||||
638
lms/static/js/writable_gradebook.js
Normal file
638
lms/static/js/writable_gradebook.js
Normal 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();
|
||||
});
|
||||
92
lms/templates/courseware/writable_gradebook.html
Normal file
92
lms/templates/courseware/writable_gradebook.html
Normal 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
25
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user