prettify old gradebook

refs GRADE-1934

test plan:
 * Verify Jenkins passes

Change-Id: Iadefa6fca5706f2e0bb67f448ada6367e71899f9
Reviewed-on: https://gerrit.instructure.com/177537
Tested-by: Jenkins
Reviewed-by: Keith Garner <kgarner@instructure.com>
QA-Review: Jeremy Neander <jneander@instructure.com>
Product-Review: Jeremy Neander <jneander@instructure.com>
This commit is contained in:
Jeremy Neander 2019-01-10 16:07:51 -06:00
parent 69218d0f8b
commit 270f95443d
30 changed files with 4256 additions and 3740 deletions

View File

@ -32,7 +32,7 @@ const GradebookRouter = Backbone.Router.extend({
'tab-:viewName': 'tab'
},
initialize () {
initialize() {
this.isLoaded = false
this.views = {}
this.views.assignment = new Gradebook(ENV.GRADEBOOK_OPTIONS)
@ -43,7 +43,7 @@ const GradebookRouter = Backbone.Router.extend({
}
},
initOutcomes () {
initOutcomes() {
const book = new OutcomeGradebookView({
el: $('.outcome-gradebook-container'),
gradebook: this.views.assignment,
@ -57,20 +57,28 @@ const GradebookRouter = Backbone.Router.extend({
renderPagination(page, pageCount) {
ReactDOM.render(
<Paginator page={page} pageCount={pageCount} loadPage={(p) => this.views.outcome.loadPage(p)} />,
document.getElementById("outcome-gradebook-paginator")
<Paginator
page={page}
pageCount={pageCount}
loadPage={p => this.views.outcome.loadPage(p)}
/>,
document.getElementById('outcome-gradebook-paginator')
)
},
handlePillChange (viewname) {
handlePillChange(viewname) {
if (viewname) this.navigate(`tab-${viewname}`, {trigger: true})
},
tab (viewName) {
tab(viewName) {
if (!viewName) viewName = userSettings.contextGet('gradebook_tab')
window.tab = viewName
if ((viewName !== 'outcome') || !this.views.outcome) { viewName = 'assignment' }
if (this.navigation) { this.navigation.setActiveView(viewName) }
if (viewName !== 'outcome' || !this.views.outcome) {
viewName = 'assignment'
}
if (this.navigation) {
this.navigation.setActiveView(viewName)
}
$('.assignment-gradebook-container, .outcome-gradebook-container').addClass('hidden')
$(`.${viewName}-gradebook-container`).removeClass('hidden')
$('#outcome-gradebook-paginator').toggleClass('hidden', viewName !== 'outcome')

View File

@ -19,123 +19,130 @@
import _ from 'underscore'
import {divide, sum, sumBy} from './shared/helpers/GradeCalculationHelper'
function partition (collection, partitionFn) {
const grouped = _.groupBy(collection, partitionFn);
return [grouped.true || [], grouped.false || []];
function partition(collection, partitionFn) {
const grouped = _.groupBy(collection, partitionFn)
return [grouped.true || [], grouped.false || []]
}
function parseScore (score) {
const result = parseFloat(score);
return (result && isFinite(result)) ? result : 0;
function parseScore(score) {
const result = parseFloat(score)
return result && isFinite(result) ? result : 0
}
function sortPairsDescending ([scoreA, submissionA], [scoreB, submissionB]) {
const scoreDiff = scoreB - scoreA;
function sortPairsDescending([scoreA, submissionA], [scoreB, submissionB]) {
const scoreDiff = scoreB - scoreA
if (scoreDiff !== 0) {
return scoreDiff;
return scoreDiff
}
// To ensure stable sorting, use the assignment id as a secondary sort.
return submissionA.assignment_id - submissionB.assignment_id;
return submissionA.assignment_id - submissionB.assignment_id
}
function sortPairsAscending ([scoreA, submissionA], [scoreB, submissionB]) {
const scoreDiff = scoreA - scoreB;
function sortPairsAscending([scoreA, submissionA], [scoreB, submissionB]) {
const scoreDiff = scoreA - scoreB
if (scoreDiff !== 0) {
return scoreDiff;
return scoreDiff
}
// To ensure stable sorting, use the assignment id as a secondary sort.
return submissionA.assignment_id - submissionB.assignment_id;
return submissionA.assignment_id - submissionB.assignment_id
}
function sortSubmissionsAscending (submissionA, submissionB) {
const scoreDiff = submissionA.score - submissionB.score;
function sortSubmissionsAscending(submissionA, submissionB) {
const scoreDiff = submissionA.score - submissionB.score
if (scoreDiff !== 0) {
return scoreDiff;
return scoreDiff
}
// To ensure stable sorting, use the assignment id as a secondary sort.
return submissionA.assignment_id - submissionB.assignment_id;
return submissionA.assignment_id - submissionB.assignment_id
}
function getSubmissionGrade ({ score, total }) {
return score / total;
function getSubmissionGrade({score, total}) {
return score / total
}
function estimateQHigh (pointed, unpointed, grades) {
function estimateQHigh(pointed, unpointed, grades) {
if (unpointed.length > 0) {
const pointsPossible = sumBy(pointed, 'total');
const bestPointedScore = Math.max(pointsPossible, sumBy(pointed, 'score'));
const unpointedScore = sumBy(unpointed, 'score');
return (bestPointedScore + unpointedScore) / pointsPossible;
const pointsPossible = sumBy(pointed, 'total')
const bestPointedScore = Math.max(pointsPossible, sumBy(pointed, 'score'))
const unpointedScore = sumBy(unpointed, 'score')
return (bestPointedScore + unpointedScore) / pointsPossible
}
return grades[grades.length - 1];
return grades[grades.length - 1]
}
function buildBigF (keepCount, cannotDrop, sortFn) {
return function bigF (q, submissions) {
const ratedScores = _.map(submissions, submission => (
[submission.score - (q * submission.total), submission]
));
const rankedScores = ratedScores.sort(sortFn);
const keptScores = rankedScores.slice(0, keepCount);
const qKept = sumBy(keptScores, ([score]) => score);
const keptSubmissions = _.map(keptScores, ([_score, submission]) => submission);
const qCannotDrop = sumBy(cannotDrop, submission => submission.score - (q * submission.total));
return [qKept + qCannotDrop, keptSubmissions];
function buildBigF(keepCount, cannotDrop, sortFn) {
return function bigF(q, submissions) {
const ratedScores = _.map(submissions, submission => [
submission.score - q * submission.total,
submission
])
const rankedScores = ratedScores.sort(sortFn)
const keptScores = rankedScores.slice(0, keepCount)
const qKept = sumBy(keptScores, ([score]) => score)
const keptSubmissions = _.map(keptScores, ([_score, submission]) => submission)
const qCannotDrop = sumBy(cannotDrop, submission => submission.score - q * submission.total)
return [qKept + qCannotDrop, keptSubmissions]
}
}
function dropPointed (droppableSubmissionData, cannotDrop, keepHighest, keepLowest) {
const totals = _.map(droppableSubmissionData, 'total');
const maxTotal = Math.max(...totals);
function dropPointed(droppableSubmissionData, cannotDrop, keepHighest, keepLowest) {
const totals = _.map(droppableSubmissionData, 'total')
const maxTotal = Math.max(...totals)
function keepHelper (submissions, initialKeepCount, bigFSort) {
const keepCount = Math.max(1, initialKeepCount);
function keepHelper(submissions, initialKeepCount, bigFSort) {
const keepCount = Math.max(1, initialKeepCount)
if (submissions.length <= keepCount) {
return submissions;
return submissions
}
const allSubmissionData = [...submissions, ...cannotDrop];
const [unpointed, pointed] = partition(allSubmissionData, submissionDatum => submissionDatum.total === 0);
const allSubmissionData = [...submissions, ...cannotDrop]
const [unpointed, pointed] = partition(
allSubmissionData,
submissionDatum => submissionDatum.total === 0
)
const grades = pointed.map(getSubmissionGrade).sort();
let qHigh = estimateQHigh(pointed, unpointed, grades);
let qLow = grades[0];
let qMid = (qLow + qHigh) / 2;
const grades = pointed.map(getSubmissionGrade).sort()
let qHigh = estimateQHigh(pointed, unpointed, grades)
let qLow = grades[0]
let qMid = (qLow + qHigh) / 2
const bigF = buildBigF(keepCount, cannotDrop, bigFSort);
const bigF = buildBigF(keepCount, cannotDrop, bigFSort)
let [x, submissionsToKeep] = bigF(qMid, submissions);
const threshold = 1 / (2 * keepCount * (maxTotal ** 2));
let [x, submissionsToKeep] = bigF(qMid, submissions)
const threshold = 1 / (2 * keepCount * maxTotal ** 2)
while (qHigh - qLow >= threshold) {
if (x < 0) {
qHigh = qMid;
qHigh = qMid
} else {
qLow = qMid;
qLow = qMid
}
qMid = (qLow + qHigh) / 2;
qMid = (qLow + qHigh) / 2
if (qMid === qHigh || qMid === qLow) {
break;
break
}
[x, submissionsToKeep] = bigF(qMid, submissions);
;[x, submissionsToKeep] = bigF(qMid, submissions)
}
return submissionsToKeep;
return submissionsToKeep
}
const submissionsWithLowestDropped = keepHelper(
droppableSubmissionData, keepHighest, sortPairsDescending
);
return keepHelper(
submissionsWithLowestDropped, keepLowest, sortPairsAscending
);
droppableSubmissionData,
keepHighest,
sortPairsDescending
)
return keepHelper(submissionsWithLowestDropped, keepLowest, sortPairsAscending)
}
function dropUnpointed (submissions, keepHighest, keepLowest) {
const sortedSubmissions = submissions.sort(sortSubmissionsAscending);
return _.chain(sortedSubmissions).last(keepHighest).first(keepLowest).value();
function dropUnpointed(submissions, keepHighest, keepLowest) {
const sortedSubmissions = submissions.sort(sortSubmissionsAscending)
return _.chain(sortedSubmissions)
.last(keepHighest)
.first(keepLowest)
.value()
}
// I am not going to pretend that this code is understandable.
@ -148,104 +155,118 @@ function dropUnpointed (submissions, keepHighest, keepLowest) {
// Grades" by Daniel Kane and Jonathan Kane. Please see that paper for
// a full explanation of the math.
// (http://cseweb.ucsd.edu/~dakane/droplowest.pdf)
function dropAssignments (allSubmissionData, rules = {}) {
let dropLowest = rules.drop_lowest || 0;
let dropHighest = rules.drop_highest || 0;
const neverDropIds = rules.never_drop || [];
function dropAssignments(allSubmissionData, rules = {}) {
let dropLowest = rules.drop_lowest || 0
let dropHighest = rules.drop_highest || 0
const neverDropIds = rules.never_drop || []
if (!(dropLowest || dropHighest)) {
return allSubmissionData;
return allSubmissionData
}
let cannotDrop = [];
let droppableSubmissionData = allSubmissionData;
let cannotDrop = []
let droppableSubmissionData = allSubmissionData
if (neverDropIds.length > 0) {
[cannotDrop, droppableSubmissionData] = partition(allSubmissionData, submission => (
;[cannotDrop, droppableSubmissionData] = partition(allSubmissionData, submission =>
_.contains(neverDropIds, submission.submission.assignment_id)
));
)
}
if (droppableSubmissionData.length === 0) {
return cannotDrop;
return cannotDrop
}
dropLowest = Math.min(dropLowest, droppableSubmissionData.length - 1);
dropHighest = (dropLowest + dropHighest) >= droppableSubmissionData.length ? 0 : dropHighest;
dropLowest = Math.min(dropLowest, droppableSubmissionData.length - 1)
dropHighest = dropLowest + dropHighest >= droppableSubmissionData.length ? 0 : dropHighest
const keepHighest = droppableSubmissionData.length - dropLowest;
const keepLowest = keepHighest - dropHighest;
const hasPointed = _.some(droppableSubmissionData, submission => submission.total > 0);
const keepHighest = droppableSubmissionData.length - dropLowest
const keepLowest = keepHighest - dropHighest
const hasPointed = _.some(droppableSubmissionData, submission => submission.total > 0)
let submissionsToKeep;
let submissionsToKeep
if (hasPointed) {
submissionsToKeep = dropPointed(droppableSubmissionData, cannotDrop, keepHighest, keepLowest);
submissionsToKeep = dropPointed(droppableSubmissionData, cannotDrop, keepHighest, keepLowest)
} else {
submissionsToKeep = dropUnpointed(droppableSubmissionData, keepHighest, keepLowest);
submissionsToKeep = dropUnpointed(droppableSubmissionData, keepHighest, keepLowest)
}
submissionsToKeep = [...submissionsToKeep, ...cannotDrop];
submissionsToKeep = [...submissionsToKeep, ...cannotDrop]
_.difference(droppableSubmissionData, submissionsToKeep).forEach((submission) => {
submission.drop = true; // eslint-disable-line no-param-reassign
});
_.difference(droppableSubmissionData, submissionsToKeep).forEach(submission => {
submission.drop = true // eslint-disable-line no-param-reassign
})
return submissionsToKeep;
return submissionsToKeep
}
function calculateGroupGrade (group, allSubmissions, includeUngraded) {
function calculateGroupGrade(group, allSubmissions, includeUngraded) {
// Remove assignments without visibility from gradeableAssignments.
const hiddenAssignmentsById = _.chain(allSubmissions).filter('hidden').indexBy('assignment_id').value();
const gradeableAssignments = _.reject(group.assignments, assignment => (
assignment.omit_from_final_grade ||
const hiddenAssignmentsById = _.chain(allSubmissions)
.filter('hidden')
.indexBy('assignment_id')
.value()
const gradeableAssignments = _.reject(
group.assignments,
assignment =>
assignment.omit_from_final_grade ||
hiddenAssignmentsById[assignment.id] ||
_.isEqual(assignment.submission_types, ['not_graded'])
));
const assignments = _.indexBy(gradeableAssignments, 'id');
)
const assignments = _.indexBy(gradeableAssignments, 'id')
// Remove submissions from other assignment groups.
let submissions = _.filter(allSubmissions, submission => assignments[submission.assignment_id]);
let submissions = _.filter(allSubmissions, submission => assignments[submission.assignment_id])
// To calculate grades for assignments to which the student has not yet
// submitted, create a submission stub with a score of `null`.
if (includeUngraded) {
const submissionAssignmentIds = _.map(submissions, ({ assignment_id }) => assignment_id.toString());
const missingAssignmentIds = _.difference(_.keys(assignments), submissionAssignmentIds);
const submissionStubs = _.map(missingAssignmentIds, assignmentId => (
{ assignment_id: assignmentId, score: null }
));
submissions = [...submissions, ...submissionStubs];
const submissionAssignmentIds = _.map(submissions, ({assignment_id}) =>
assignment_id.toString()
)
const missingAssignmentIds = _.difference(_.keys(assignments), submissionAssignmentIds)
const submissionStubs = _.map(missingAssignmentIds, assignmentId => ({
assignment_id: assignmentId,
score: null
}))
submissions = [...submissions, ...submissionStubs]
}
// Remove excused submissions.
submissions = _.reject(submissions, 'excused');
submissions = _.reject(submissions, 'excused')
const submissionData = _.map(submissions, submission => (
{
total: parseScore(assignments[submission.assignment_id].points_possible),
score: parseScore(submission.score),
submitted: submission.score != null && submission.score !== '',
pending_review: submission.workflow_state === 'pending_review',
submission
}
));
const submissionData = _.map(submissions, submission => ({
total: parseScore(assignments[submission.assignment_id].points_possible),
score: parseScore(submission.score),
submitted: submission.score != null && submission.score !== '',
pending_review: submission.workflow_state === 'pending_review',
submission
}))
let relevantSubmissionData = submissionData;
let relevantSubmissionData = submissionData
if (!includeUngraded) {
relevantSubmissionData = _.filter(submissionData, submission => (
submission.submitted && !submission.pending_review
));
relevantSubmissionData = _.filter(
submissionData,
submission => submission.submitted && !submission.pending_review
)
}
const submissionsToKeep = dropAssignments(relevantSubmissionData, group.rules);
const score = sum(_.chain(submissionsToKeep).map('score').map(parseScore).value());
const possible = sumBy(submissionsToKeep, 'total');
const submissionsToKeep = dropAssignments(relevantSubmissionData, group.rules)
const score = sum(
_.chain(submissionsToKeep)
.map('score')
.map(parseScore)
.value()
)
const possible = sumBy(submissionsToKeep, 'total')
return {
score,
possible,
submission_count: _.filter(submissionData, 'submitted').length,
submissions: _.map(submissionData, submissionDatum => {
const percent = submissionDatum.total ? divide(submissionDatum.score, submissionDatum.total) : 0
const percent = submissionDatum.total
? divide(submissionDatum.score, submissionDatum.total)
: 0
return {
drop: submissionDatum.drop,
percent: parseScore(percent),
@ -255,7 +276,7 @@ function calculateGroupGrade (group, allSubmissions, includeUngraded) {
submitted: submissionDatum.submitted
}
})
};
}
}
// Each submission requires the following properties:
@ -300,15 +321,15 @@ function calculateGroupGrade (group, allSubmissions, includeUngraded) {
// final: <AssignmentGroup Grade *see above>
// scoreUnit: 'points'
// }
function calculate (allSubmissions, assignmentGroup) {
const submissions = _.uniq(allSubmissions, 'assignment_id');
function calculate(allSubmissions, assignmentGroup) {
const submissions = _.uniq(allSubmissions, 'assignment_id')
return {
assignmentGroupId: assignmentGroup.id,
assignmentGroupWeight: assignmentGroup.group_weight,
current: calculateGroupGrade(assignmentGroup, submissions, false),
final: calculateGroupGrade(assignmentGroup, submissions, true),
scoreUnit: 'points'
};
}
}
export default {

View File

@ -19,72 +19,80 @@
import _ from 'underscore'
import tz from 'timezone'
function addStudentID(student, collection = []) {
return collection.concat([student.id]);
function addStudentID(student, collection = []) {
return collection.concat([student.id])
}
function studentIDCollections(students) {
const sections = {}
const groups = {}
_.each(students, function(student) {
_.each(
student.sections,
sectionID => (sections[sectionID] = addStudentID(student, sections[sectionID]))
)
_.each(student.group_ids, groupID => (groups[groupID] = addStudentID(student, groups[groupID])))
})
return {studentIDsInSections: sections, studentIDsInGroups: groups}
}
function studentIDsOnOverride(override, sections, groups) {
if (override.student_ids) {
return override.student_ids
} else if (override.course_section_id && sections[override.course_section_id]) {
return sections[override.course_section_id]
} else if (override.group_id && groups[override.group_id]) {
return groups[override.group_id]
} else {
return []
}
}
function studentIDCollections(students) {
const sections = {};
const groups = {};
_.each(students, function(student) {
_.each(student.sections, sectionID => sections[sectionID] = addStudentID(student, sections[sectionID]));
_.each(student.group_ids, groupID => groups[groupID] = addStudentID(student, groups[groupID]));
});
return { studentIDsInSections: sections, studentIDsInGroups: groups };
function getLatestDefinedDate(newDate, existingDate) {
if (existingDate === undefined || newDate === null) {
return newDate
} else if (existingDate !== null && newDate > existingDate) {
return newDate
} else {
return existingDate
}
}
function studentIDsOnOverride(override, sections, groups) {
if (override.student_ids) {
return override.student_ids;
} else if (override.course_section_id && sections[override.course_section_id]) {
return sections[override.course_section_id];
} else if (override.group_id && groups[override.group_id]) {
return groups[override.group_id];
} else {
return [];
function effectiveDueDatesOnOverride(
studentIDsInSections,
studentIDsInGroups,
studentDueDateMap,
override
) {
const studentIDs = studentIDsOnOverride(override, studentIDsInSections, studentIDsInGroups)
_.each(studentIDs, function(studentID) {
const existingDate = studentDueDateMap[studentID]
const newDate = tz.parse(override.due_at)
studentDueDateMap[studentID] = getLatestDefinedDate(newDate, existingDate)
})
return studentDueDateMap
}
function effectiveDueDatesForAssignment(assignment, overrides, students) {
const {studentIDsInSections, studentIDsInGroups} = studentIDCollections(students)
const dates = _.reduce(
overrides,
effectiveDueDatesOnOverride.bind(this, studentIDsInSections, studentIDsInGroups),
{}
)
_.each(students, function(student) {
if (dates[student.id] === undefined && !assignment.only_visible_to_overrides) {
dates[student.id] = tz.parse(assignment.due_at)
}
}
})
function getLatestDefinedDate(newDate, existingDate) {
if (existingDate === undefined || newDate === null) {
return newDate;
} else if (existingDate !== null && newDate > existingDate) {
return newDate;
} else {
return existingDate;
}
}
return dates
}
function effectiveDueDatesOnOverride(studentIDsInSections, studentIDsInGroups, studentDueDateMap, override) {
const studentIDs = studentIDsOnOverride(override, studentIDsInSections, studentIDsInGroups);
_.each(studentIDs, function(studentID) {
const existingDate = studentDueDateMap[studentID];
const newDate = tz.parse(override.due_at);
studentDueDateMap[studentID] = getLatestDefinedDate(newDate, existingDate);
});
return studentDueDateMap;
}
function effectiveDueDatesForAssignment(assignment, overrides, students) {
const { studentIDsInSections, studentIDsInGroups } = studentIDCollections(students);
const dates = _.reduce(
overrides,
effectiveDueDatesOnOverride.bind(this, studentIDsInSections, studentIDsInGroups),
{}
);
_.each(students, function(student) {
if (dates[student.id] === undefined && !assignment.only_visible_to_overrides) {
dates[student.id] = tz.parse(assignment.due_at);
}
});
return dates;
}
export default { effectiveDueDatesForAssignment }
export default {effectiveDueDatesForAssignment}

View File

@ -19,31 +19,37 @@
import _ from 'underscore'
import round from 'compiled/util/round'
import AssignmentGroupGradeCalculator from '../gradebook/AssignmentGroupGradeCalculator'
import {bigSum, sum, sumBy, toNumber, weightedPercent} from './shared/helpers/GradeCalculationHelper'
import {
bigSum,
sum,
sumBy,
toNumber,
weightedPercent
} from './shared/helpers/GradeCalculationHelper'
function combineAssignmentGroupGrades (assignmentGroupGrades, includeUngraded, options) {
const scopedAssignmentGroupGrades = _.map(assignmentGroupGrades, (assignmentGroupGrade) => {
const gradeVersion = includeUngraded ? assignmentGroupGrade.final : assignmentGroupGrade.current;
return { ...gradeVersion, weight: assignmentGroupGrade.assignmentGroupWeight };
});
function combineAssignmentGroupGrades(assignmentGroupGrades, includeUngraded, options) {
const scopedAssignmentGroupGrades = _.map(assignmentGroupGrades, assignmentGroupGrade => {
const gradeVersion = includeUngraded ? assignmentGroupGrade.final : assignmentGroupGrade.current
return {...gradeVersion, weight: assignmentGroupGrade.assignmentGroupWeight}
})
if (options.weightAssignmentGroups) {
const relevantGroupGrades = scopedAssignmentGroupGrades.filter(grade => grade.possible);
const fullWeight = sumBy(relevantGroupGrades, 'weight');
const relevantGroupGrades = scopedAssignmentGroupGrades.filter(grade => grade.possible)
const fullWeight = sumBy(relevantGroupGrades, 'weight')
let finalGrade = bigSum(_.map(relevantGroupGrades, weightedPercent));
let finalGrade = bigSum(_.map(relevantGroupGrades, weightedPercent))
if (fullWeight === 0) {
finalGrade = null;
finalGrade = null
} else if (fullWeight < 100) {
finalGrade = toNumber(weightedPercent({score: finalGrade, possible: fullWeight, weight: 100}));
finalGrade = toNumber(weightedPercent({score: finalGrade, possible: fullWeight, weight: 100}))
}
const submissionCount = sumBy(relevantGroupGrades, 'submission_count');
const possible = ((submissionCount > 0) || includeUngraded) ? 100 : 0;
let score = finalGrade && round(finalGrade, 2);
score = isNaN(score) ? null : score;
const submissionCount = sumBy(relevantGroupGrades, 'submission_count')
const possible = submissionCount > 0 || includeUngraded ? 100 : 0
let score = finalGrade && round(finalGrade, 2)
score = isNaN(score) ? null : score
return { score, possible };
return {score, possible}
}
return {
@ -52,63 +58,68 @@ function combineAssignmentGroupGrades (assignmentGroupGrades, includeUngraded, o
}
}
function combineGradingPeriodGrades (gradingPeriodGradesByPeriodId, includeUngraded) {
let scopedGradingPeriodGrades = _.map(gradingPeriodGradesByPeriodId, (gradingPeriodGrade) => {
const gradeVersion = includeUngraded ? gradingPeriodGrade.final : gradingPeriodGrade.current;
return { ...gradeVersion, weight: gradingPeriodGrade.gradingPeriodWeight };
});
function combineGradingPeriodGrades(gradingPeriodGradesByPeriodId, includeUngraded) {
let scopedGradingPeriodGrades = _.map(gradingPeriodGradesByPeriodId, gradingPeriodGrade => {
const gradeVersion = includeUngraded ? gradingPeriodGrade.final : gradingPeriodGrade.current
return {...gradeVersion, weight: gradingPeriodGrade.gradingPeriodWeight}
})
if (!includeUngraded) {
scopedGradingPeriodGrades = _.filter(scopedGradingPeriodGrades, 'possible');
scopedGradingPeriodGrades = _.filter(scopedGradingPeriodGrades, 'possible')
}
const scoreSum = bigSum(_.map(scopedGradingPeriodGrades, weightedPercent));
const totalWeight = sumBy(scopedGradingPeriodGrades, 'weight');
const totalScore = totalWeight === 0 ? 0 : toNumber(scoreSum.times(100).div(Math.min(totalWeight, 100)));
const scoreSum = bigSum(_.map(scopedGradingPeriodGrades, weightedPercent))
const totalWeight = sumBy(scopedGradingPeriodGrades, 'weight')
const totalScore =
totalWeight === 0 ? 0 : toNumber(scoreSum.times(100).div(Math.min(totalWeight, 100)))
return {
score: round(totalScore, 2),
possible: 100
};
}
}
function divideGroupByGradingPeriods (assignmentGroup, effectiveDueDates) {
function divideGroupByGradingPeriods(assignmentGroup, effectiveDueDates) {
// When using weighted grading periods, assignment groups must not contain assignments due in different grading
// periods. This allows for calculated assignment group grades in closed grading periods to be accidentally
// changed if a related assignment is considered to be in an open grading period.
//
// To avoid this, assignment groups meeting this criteria are "divided" (duplicated) in a way where each
// instance of the assignment group includes assignments only from one grading period.
const assignmentsByGradingPeriodId = _.groupBy(assignmentGroup.assignments, assignment => (
effectiveDueDates[assignment.id].grading_period_id
));
return _.map(assignmentsByGradingPeriodId, assignments => (
{ ...assignmentGroup, assignments }
));
const assignmentsByGradingPeriodId = _.groupBy(
assignmentGroup.assignments,
assignment => effectiveDueDates[assignment.id].grading_period_id
)
return _.map(assignmentsByGradingPeriodId, assignments => ({...assignmentGroup, assignments}))
}
function extractPeriodBasedAssignmentGroups (assignmentGroups, effectiveDueDates) {
return _.reduce(assignmentGroups, (periodBasedGroups, assignmentGroup) => {
const assignedAssignments = _.filter(assignmentGroup.assignments, assignment => (
effectiveDueDates[assignment.id]
));
if (assignedAssignments.length > 0) {
const groupWithAssignedAssignments = { ...assignmentGroup, assignments: assignedAssignments };
return [
...periodBasedGroups,
...divideGroupByGradingPeriods(groupWithAssignedAssignments, effectiveDueDates)
];
}
return periodBasedGroups;
}, []);
function extractPeriodBasedAssignmentGroups(assignmentGroups, effectiveDueDates) {
return _.reduce(
assignmentGroups,
(periodBasedGroups, assignmentGroup) => {
const assignedAssignments = _.filter(
assignmentGroup.assignments,
assignment => effectiveDueDates[assignment.id]
)
if (assignedAssignments.length > 0) {
const groupWithAssignedAssignments = {...assignmentGroup, assignments: assignedAssignments}
return [
...periodBasedGroups,
...divideGroupByGradingPeriods(groupWithAssignedAssignments, effectiveDueDates)
]
}
return periodBasedGroups
},
[]
)
}
function recombinePeriodBasedAssignmentGroupGrades (grades) {
const map = {};
function recombinePeriodBasedAssignmentGroupGrades(grades) {
const map = {}
for (let g = 0; g < grades.length; g++) {
const grade = grades[g]
const previousGrade = map[grade.assignmentGroupId];
const previousGrade = map[grade.assignmentGroupId]
if (previousGrade) {
map[grade.assignmentGroupId] = {
@ -125,40 +136,49 @@ function recombinePeriodBasedAssignmentGroupGrades (grades) {
score: sum([previousGrade.final.score, grade.final.score]),
possible: sum([previousGrade.final.possible, grade.final.possible])
}
};
}
} else {
map[grade.assignmentGroupId] = grade;
map[grade.assignmentGroupId] = grade
}
}
return map;
return map
}
function calculateWithGradingPeriods (submissions, assignmentGroups, gradingPeriods, effectiveDueDates, options) {
const periodBasedGroups = extractPeriodBasedAssignmentGroups(assignmentGroups, effectiveDueDates);
function calculateWithGradingPeriods(
submissions,
assignmentGroups,
gradingPeriods,
effectiveDueDates,
options
) {
const periodBasedGroups = extractPeriodBasedAssignmentGroups(assignmentGroups, effectiveDueDates)
const assignmentGroupsByGradingPeriodId = _.groupBy(periodBasedGroups, (assignmentGroup) => {
const assignmentId = assignmentGroup.assignments[0].id;
return effectiveDueDates[assignmentId].grading_period_id;
});
const assignmentGroupsByGradingPeriodId = _.groupBy(periodBasedGroups, assignmentGroup => {
const assignmentId = assignmentGroup.assignments[0].id
return effectiveDueDates[assignmentId].grading_period_id
})
const gradingPeriodsById = _.indexBy(gradingPeriods, 'id');
const gradingPeriodGradesByPeriodId = {};
const periodBasedAssignmentGroupGrades = [];
const gradingPeriodsById = _.indexBy(gradingPeriods, 'id')
const gradingPeriodGradesByPeriodId = {}
const periodBasedAssignmentGroupGrades = []
for (let gp = 0; gp < gradingPeriods.length; gp++) {
const groupGrades = {};
const groupGrades = {}
const gradingPeriod = gradingPeriods[gp]
const assignmentGroupsInPeriod = assignmentGroupsByGradingPeriodId[gradingPeriod.id] || []
for (let ag = 0; ag < assignmentGroupsInPeriod.length; ag++) {
const assignmentGroup = assignmentGroupsInPeriod[ag]
groupGrades[assignmentGroup.id] = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup);
periodBasedAssignmentGroupGrades.push(groupGrades[assignmentGroup.id]);
groupGrades[assignmentGroup.id] = AssignmentGroupGradeCalculator.calculate(
submissions,
assignmentGroup
)
periodBasedAssignmentGroupGrades.push(groupGrades[assignmentGroup.id])
}
const groupGradesList = _.values(groupGrades);
const groupGradesList = _.values(groupGrades)
gradingPeriodGradesByPeriodId[gradingPeriod.id] = {
gradingPeriodId: gradingPeriod.id,
@ -167,7 +187,7 @@ function calculateWithGradingPeriods (submissions, assignmentGroups, gradingPeri
current: combineAssignmentGroupGrades(groupGradesList, false, options),
final: combineAssignmentGroupGrades(groupGradesList, true, options),
scoreUnit: options.weightAssignmentGroups ? 'percentage' : 'points'
};
}
}
if (options.weightGradingPeriods) {
@ -177,12 +197,12 @@ function calculateWithGradingPeriods (submissions, assignmentGroups, gradingPeri
current: combineGradingPeriodGrades(gradingPeriodGradesByPeriodId, false, options),
final: combineGradingPeriodGrades(gradingPeriodGradesByPeriodId, true, options),
scoreUnit: 'percentage'
};
}
}
const allAssignmentGroupGrades = _.map(assignmentGroups, assignmentGroup => (
const allAssignmentGroupGrades = _.map(assignmentGroups, assignmentGroup =>
AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup)
));
)
return {
assignmentGroups: _.indexBy(allAssignmentGroupGrades, grade => grade.assignmentGroupId),
@ -190,20 +210,20 @@ function calculateWithGradingPeriods (submissions, assignmentGroups, gradingPeri
current: combineAssignmentGroupGrades(allAssignmentGroupGrades, false, options),
final: combineAssignmentGroupGrades(allAssignmentGroupGrades, true, options),
scoreUnit: options.weightAssignmentGroups ? 'percentage' : 'points'
};
}
}
function calculateWithoutGradingPeriods (submissions, assignmentGroups, options) {
const assignmentGroupGrades = _.map(assignmentGroups, assignmentGroup => (
function calculateWithoutGradingPeriods(submissions, assignmentGroups, options) {
const assignmentGroupGrades = _.map(assignmentGroups, assignmentGroup =>
AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup)
));
)
return {
assignmentGroups: _.indexBy(assignmentGroupGrades, grade => grade.assignmentGroupId),
current: combineAssignmentGroupGrades(assignmentGroupGrades, false, options),
final: combineAssignmentGroupGrades(assignmentGroupGrades, true, options),
scoreUnit: options.weightAssignmentGroups ? 'percentage' : 'points'
};
}
}
// Each submission requires the following properties:
@ -304,19 +324,29 @@ function calculateWithoutGradingPeriods (submissions, assignmentGroups, options)
// final: <AssignmentGroup Grade *see above>
// scoreUnit: 'points'|'percent'
// }
function calculate (submissions, assignmentGroups, weightingScheme, gradingPeriodSet, effectiveDueDates) {
function calculate(
submissions,
assignmentGroups,
weightingScheme,
gradingPeriodSet,
effectiveDueDates
) {
const options = {
weightGradingPeriods: gradingPeriodSet && !!gradingPeriodSet.weighted,
weightAssignmentGroups: weightingScheme === 'percent'
};
}
if (gradingPeriodSet && effectiveDueDates) {
return calculateWithGradingPeriods(
submissions, assignmentGroups, gradingPeriodSet.gradingPeriods, effectiveDueDates, options
);
submissions,
assignmentGroups,
gradingPeriodSet.gradingPeriods,
effectiveDueDates,
options
)
}
return calculateWithoutGradingPeriods(submissions, assignmentGroups, options);
return calculateWithoutGradingPeriods(submissions, assignmentGroups, options)
}
export default {

View File

@ -20,135 +20,145 @@ import $ from 'jquery'
import cheaterDepaginate from '../shared/CheatDepaginator'
import _ from 'underscore'
function getGradingPeriodAssignments (courseId) {
const url = `/courses/${courseId}/gradebook/grading_period_assignments`;
return $.ajaxJSON(url, 'GET', {});
function getGradingPeriodAssignments(courseId) {
const url = `/courses/${courseId}/gradebook/grading_period_assignments`
return $.ajaxJSON(url, 'GET', {})
}
// loaders
const getAssignmentGroups = (url, params) => {
return cheaterDepaginate(url, params);
};
const getCustomColumns = (url) => {
return $.ajaxJSON(url, "GET", {});
};
const getSections = (url) => {
return $.ajaxJSON(url, "GET", {});
};
// loaders
const getAssignmentGroups = (url, params) => {
return cheaterDepaginate(url, params)
}
const getCustomColumns = url => {
return $.ajaxJSON(url, 'GET', {})
}
const getSections = url => {
return $.ajaxJSON(url, 'GET', {})
}
// submission loading is tricky
let pendingStudentsForSubmissions;
let submissionsLoaded;
let studentsLoaded;
let submissionChunkCount;
let gotSubmissionChunkCount;
let submissionsLoading = false;
let submissionURL;
let submissionParams;
let submissionChunkSize;
let submissionChunkCb;
// submission loading is tricky
let pendingStudentsForSubmissions
let submissionsLoaded
let studentsLoaded
let submissionChunkCount
let gotSubmissionChunkCount
let submissionsLoading = false
let submissionURL
let submissionParams
let submissionChunkSize
let submissionChunkCb
const gotSubmissionsChunk = (data) => {
gotSubmissionChunkCount++;
submissionChunkCb(data);
const gotSubmissionsChunk = data => {
gotSubmissionChunkCount++
submissionChunkCb(data)
if (gotSubmissionChunkCount === submissionChunkCount &&
studentsLoaded.isResolved()) {
submissionsLoaded.resolve();
if (gotSubmissionChunkCount === submissionChunkCount && studentsLoaded.isResolved()) {
submissionsLoaded.resolve()
}
}
const getPendingSubmissions = () => {
while (pendingStudentsForSubmissions.length) {
const studentIds = pendingStudentsForSubmissions.splice(0, submissionChunkSize)
submissionChunkCount++
cheaterDepaginate(submissionURL, {student_ids: studentIds, ...submissionParams}).then(
gotSubmissionsChunk
)
}
}
const getSubmissions = (url, params, cb, chunkSize) => {
submissionURL = url
submissionParams = params
submissionChunkCb = cb
submissionChunkSize = chunkSize
submissionsLoaded = $.Deferred()
submissionChunkCount = 0
gotSubmissionChunkCount = 0
submissionsLoading = true
getPendingSubmissions()
return submissionsLoaded
}
const getStudents = (url, params, studentChunkCb) => {
pendingStudentsForSubmissions = []
const gotStudentPage = students => {
studentChunkCb(students)
const studentIds = _.pluck(students, 'id')
;[].push.apply(pendingStudentsForSubmissions, studentIds)
if (submissionsLoading) {
getPendingSubmissions()
}
};
}
const getPendingSubmissions = () => {
while (pendingStudentsForSubmissions.length) {
const studentIds = pendingStudentsForSubmissions.splice(0, submissionChunkSize);
submissionChunkCount++;
cheaterDepaginate(submissionURL, { student_ids: studentIds, ...submissionParams }).then(gotSubmissionsChunk);
}
};
studentsLoaded = cheaterDepaginate(url, params, gotStudentPage)
return studentsLoaded
}
const getSubmissions = (url, params, cb, chunkSize) => {
submissionURL = url;
submissionParams = params;
submissionChunkCb = cb;
submissionChunkSize = chunkSize;
const getDataForColumn = (column, url, params, cb) => {
url = url.replace(/:id/, column.id)
const augmentedCallback = data => cb(column, data)
return cheaterDepaginate(url, params, augmentedCallback)
}
submissionsLoaded = $.Deferred();
submissionChunkCount = 0;
gotSubmissionChunkCount = 0;
const getCustomColumnData = (url, params, cb, customColumnsDfd, waitForDfds) => {
const customColumnDataLoaded = $.Deferred()
let customColumnDataDfds
submissionsLoading = true;
getPendingSubmissions();
return submissionsLoaded;
};
// waitForDfds ensures that custom column data is loaded *last*
$.when.apply($, waitForDfds).then(() => {
customColumnsDfd.then(customColumns => {
customColumnDataDfds = customColumns.map(col => getDataForColumn(col, url, params, cb))
})
})
const getStudents = (url, params, studentChunkCb) => {
pendingStudentsForSubmissions = [];
$.when.apply($, customColumnDataDfds).then(() => customColumnDataLoaded.resolve())
const gotStudentPage = (students) => {
studentChunkCb(students);
return customColumnDataLoaded
}
const studentIds = _.pluck(students, 'id');
[].push.apply(pendingStudentsForSubmissions, studentIds);
const loadGradebookData = opts => {
const gotAssignmentGroups = getAssignmentGroups(
opts.assignmentGroupsURL,
opts.assignmentGroupsParams
)
if (opts.onlyLoadAssignmentGroups) {
return {gotAssignmentGroups}
}
if (submissionsLoading) {
getPendingSubmissions();
}
};
let gotGradingPeriodAssignments
if (opts.getGradingPeriodAssignments) {
gotGradingPeriodAssignments = getGradingPeriodAssignments(opts.courseId)
}
const gotCustomColumns = getCustomColumns(opts.customColumnsURL)
const gotStudents = getStudents(opts.studentsURL, opts.studentsParams, opts.studentsPageCb)
const gotSubmissions = getSubmissions(
opts.submissionsURL,
opts.submissionsParams,
opts.submissionsChunkCb,
opts.submissionsChunkSize
)
const gotCustomColumnData = getCustomColumnData(
opts.customColumnDataURL,
opts.customColumnDataParams,
opts.customColumnDataPageCb,
gotCustomColumns,
[gotSubmissions]
)
studentsLoaded = cheaterDepaginate(url, params, gotStudentPage);
return studentsLoaded;
};
return {
gotAssignmentGroups,
gotCustomColumns,
gotGradingPeriodAssignments,
gotStudents,
gotSubmissions,
gotCustomColumnData
}
}
const getDataForColumn = (column, url, params, cb) => {
url = url.replace(/:id/, column.id);
const augmentedCallback = (data) => cb(column, data);
return cheaterDepaginate(url, params, augmentedCallback);
};
const getCustomColumnData = (url, params, cb, customColumnsDfd, waitForDfds) => {
const customColumnDataLoaded = $.Deferred();
let customColumnDataDfds;
// waitForDfds ensures that custom column data is loaded *last*
$.when.apply($, waitForDfds).then(() => {
customColumnsDfd.then(customColumns => {
customColumnDataDfds = customColumns.map(col => getDataForColumn(col, url, params, cb));
});
});
$.when.apply($, customColumnDataDfds)
.then(() => customColumnDataLoaded.resolve());
return customColumnDataLoaded;
};
const loadGradebookData = (opts) => {
const gotAssignmentGroups = getAssignmentGroups(opts.assignmentGroupsURL, opts.assignmentGroupsParams);
if (opts.onlyLoadAssignmentGroups) {
return { gotAssignmentGroups };
}
let gotGradingPeriodAssignments;
if (opts.getGradingPeriodAssignments) {
gotGradingPeriodAssignments = getGradingPeriodAssignments(opts.courseId);
}
const gotCustomColumns = getCustomColumns(opts.customColumnsURL);
const gotStudents = getStudents(opts.studentsURL, opts.studentsParams, opts.studentsPageCb);
const gotSubmissions = getSubmissions(opts.submissionsURL, opts.submissionsParams, opts.submissionsChunkCb, opts.submissionsChunkSize);
const gotCustomColumnData = getCustomColumnData(opts.customColumnDataURL,
opts.customColumnDataParams,
opts.customColumnDataPageCb,
gotCustomColumns,
[gotSubmissions]);
return {
gotAssignmentGroups,
gotCustomColumns,
gotGradingPeriodAssignments,
gotStudents,
gotSubmissions,
gotCustomColumnData
};
};
export default { loadGradebookData: loadGradebookData, getDataForColumn: getDataForColumn }
export default {loadGradebookData: loadGradebookData, getDataForColumn: getDataForColumn}

View File

@ -16,43 +16,43 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import _ from 'lodash';
import timezone from 'timezone';
import GradingPeriodsHelper from '../grading/helpers/GradingPeriodsHelper';
import _ from 'lodash'
import timezone from 'timezone'
import GradingPeriodsHelper from '../grading/helpers/GradingPeriodsHelper'
export function scopeToUser (dueDateData, userId) {
const scopedData = {};
export function scopeToUser(dueDateData, userId) {
const scopedData = {}
_.forEach(dueDateData, (dueDateDataByUserId, assignmentId) => {
if (dueDateDataByUserId[userId]) {
scopedData[assignmentId] = dueDateDataByUserId[userId];
scopedData[assignmentId] = dueDateDataByUserId[userId]
}
});
return scopedData;
})
return scopedData
}
export function updateWithSubmissions (effectiveDueDates, submissions, gradingPeriods = []) {
const helper = new GradingPeriodsHelper(gradingPeriods);
const sortedPeriods = _.sortBy(gradingPeriods, 'startDate');
export function updateWithSubmissions(effectiveDueDates, submissions, gradingPeriods = []) {
const helper = new GradingPeriodsHelper(gradingPeriods)
const sortedPeriods = _.sortBy(gradingPeriods, 'startDate')
submissions.forEach((submission) => {
const dueDate = timezone.parse(submission.cached_due_date);
submissions.forEach(submission => {
const dueDate = timezone.parse(submission.cached_due_date)
let gradingPeriod = null;
let gradingPeriod = null
if (gradingPeriods.length) {
if (dueDate) {
gradingPeriod = helper.gradingPeriodForDueAt(dueDate);
gradingPeriod = helper.gradingPeriodForDueAt(dueDate)
} else {
gradingPeriod = sortedPeriods[sortedPeriods.length - 1];
gradingPeriod = sortedPeriods[sortedPeriods.length - 1]
}
}
const assignmentDueDates = effectiveDueDates[submission.assignment_id] || {};
const assignmentDueDates = effectiveDueDates[submission.assignment_id] || {}
assignmentDueDates[submission.user_id] = {
due_at: submission.cached_due_date,
grading_period_id: gradingPeriod ? gradingPeriod.id : null,
in_closed_grading_period: gradingPeriod ? gradingPeriod.isClosed : false
};
}
effectiveDueDates[submission.assignment_id] = assignmentDueDates; // eslint-disable-line no-param-reassign
});
effectiveDueDates[submission.assignment_id] = assignmentDueDates // eslint-disable-line no-param-reassign
})
}

View File

@ -135,7 +135,8 @@ class PostGradesDialogCorrectionsPage extends React.Component {
onClick={this.ignoreErrorsThenProceed}
>
{errorCount > 0 ? I18n.t('Ignore These') : I18n.t('Continue')}
&nbsp;<i className="icon-arrow-right" />
&nbsp;
<i className="icon-arrow-right" />
</button>
</div>
</form>

View File

@ -63,7 +63,8 @@ class PostGradesDialogNeedsGradingPage extends React.Component {
className="btn btn-primary"
onClick={this.props.leaveNeedsGradingPage}
>
{I18n.t('Continue')}&nbsp;<i className="icon-arrow-right" />
{I18n.t('Continue')}&nbsp;
<i className="icon-arrow-right" />
</button>
</div>
</form>

View File

@ -21,218 +21,239 @@ import _ from 'underscore'
import createStore from '../../shared/helpers/createStore'
import assignmentUtils from '../../gradebook/SISGradePassback/assignmentUtils'
var PostGradesStore = (state) => {
var store = $.extend(createStore(state), {
var PostGradesStore = state => {
var store = $.extend(createStore(state), {
reset() {
var assignments = this.getAssignments()
_.each(assignments, a => (a.please_ignore = false))
this.setState({
assignments: assignments,
pleaseShowNeedsGradingPage: false
})
},
reset () {
var assignments = this.getAssignments()
_.each(assignments, (a) => a.please_ignore = false)
this.setState({
assignments: assignments,
pleaseShowNeedsGradingPage: false
})
},
hasAssignments() {
var assignments = this.getAssignments()
if (assignments != undefined && assignments.length > 0) {
return true
} else {
return false
}
},
hasAssignments () {
var assignments = this.getAssignments()
if (assignments != undefined && assignments.length > 0) {
return true
} else {
return false
}
},
getSISSectionId(section_id) {
var sections = this.getState().sections
return sections && sections[section_id] ? sections[section_id].sis_section_id : null
},
getSISSectionId (section_id) {
var sections = this.getState().sections
return (sections && sections[section_id]) ?
sections[section_id].sis_section_id :
null;
},
allOverrideIds(a) {
var overrides = []
_.each(a.overrides, o => {
overrides.push(o.course_section_id)
})
return overrides
},
allOverrideIds(a) {
var overrides = []
_.each(a.overrides, (o) => {
overrides.push(o.course_section_id)
})
return overrides
},
overrideForEveryone(a) {
var overrides = this.allOverrideIds(a)
var sections = _.keys(this.getState().sections)
var section_ids_with_no_overrides = $(sections)
.not(overrides)
.get()
overrideForEveryone(a) {
var overrides = this.allOverrideIds(a)
var sections = _.keys(this.getState().sections)
var section_ids_with_no_overrides = $(sections).not(overrides).get();
var section_for_everyone = _.find(section_ids_with_no_overrides, o => {
return state.selected.id == o
})
return section_for_everyone
},
var section_for_everyone = _.find(section_ids_with_no_overrides, (o) => {
return state.selected.id == o
});
return section_for_everyone
},
setGradeBookAssignments (gradebookAssignments) {
var assignments = []
for (var id in gradebookAssignments) {
var gba = gradebookAssignments[id]
// Only accept assignments suitable to post, e.g. published, post_to_sis
if (assignmentUtils.suitableToPost(gba)) {
// Push a copy, and only of relevant attributes
assignments.push(assignmentUtils.copyFromGradebook(gba))
}
}
// A second loop is needed to ensure non-unique name errors are included
// in hasError
_.each(assignments, (a) => {
a.original_error = assignmentUtils.hasError(assignments, a)
})
this.setState({ assignments: assignments })
},
setSections (sections) {
this.setState({ sections: sections })
this.setSelectedSection( this.getState().sectionToShow )
},
validCheck(a) {
if(a.overrideForThisSection != undefined && a.currentlySelected.type == 'course' && a.currentlySelected.id.toString() == a.overrideForThisSection.course_section_id){
return a.due_at != null ? true : false
}
else if(a.overrideForThisSection != undefined && a.currentlySelected.type == 'section' && a.currentlySelected.id.toString() == a.overrideForThisSection.course_section_id){
return a.overrideForThisSection.due_at != null ? true : false
}
else{
return true
}
},
getAssignments() {
var assignments = this.getState().assignments
var state = this.getState()
if (state.selected.type == "section") {
_.each(assignments, (a) => {
a.recentlyUpdated = false
a.currentlySelected = state.selected
a.sectionCount = _.keys(state.sections).length
a.overrideForThisSection = _.find(a.overrides, (override) => {
return override.course_section_id == state.selected.id;
});
//Handle assignment with overrides and the 'Everyone Else' scenario with a section that does not have any overrides
//cleanup overrideForThisSection logic
if(a.overrideForThisSection == undefined){ a.selectedSectionForEveryone = this.overrideForEveryone(a) }
});
} else {
_.each(assignments, (a) => {
a.recentlyUpdated = false
a.currentlySelected = state.selected
a.sectionCount = _.keys(state.sections).length
//Course is currentlySlected with sections that have overrides AND are invalid
a.overrideForThisSection = _.find(a.overrides, (override) => {
return override.due_at == null || typeof(override.due_at) == 'object';
});
//Handle assignment with overrides and the 'Everyone Else' scenario with the course currentlySelected
if(a.overrideForThisSection == undefined){ a.selectedSectionForEveryone = this.overrideForEveryone(a) }
});
}
return assignments;
},
getAssignment(assignment_id) {
var assignments = this.getAssignments()
return _.find(assignments, (a) => a.id == assignment_id)
},
setSelectedSection (section) {
var state = this.getState()
var section_id = parseInt(section)
var selected;
if (section) {
selected = {
type: "section",
id: section_id,
sis_id: this.getSISSectionId(section_id)
};
} else {
selected = {
type: "course",
id: state.course.id,
sis_id: state.course.sis_id
};
}
this.setState({ selected: selected, sectionToShow: section })
},
updateAssignment (assignment_id, newAttrs) {
var assignments = this.getAssignments()
var assignment = _.find(assignments, (a) => a.id == assignment_id)
$.extend(assignment, newAttrs)
this.setState({assignments: assignments})
},
updateAssignmentDate(assignment_id, date){
var assignments = this.getState().assignments
var assignment = _.find(assignments, (a) => a.id == assignment_id)
//the assignment has an override and the override being updated is for the section that is currentlySelected update it
if(assignment.overrideForThisSection != undefined && assignment.currentlySelected.id.toString() == assignment.overrideForThisSection.course_section_id) {
assignment.overrideForThisSection.due_at = date
assignment.please_ignore = false
assignment.hadOriginalErrors = true
this.setState({assignments: assignments})
}
//the section override being set from the course level of the sction dropdown
else if(assignment.overrideForThisSection != undefined && assignment.currentlySelected.id.toString() != assignment.overrideForThisSection.course_section_id){
assignment.overrideForThisSection.due_at = date
assignment.please_ignore = false
assignment.hadOriginalErrors = true
this.setState({assignments: assignments})
}
//update normal assignment and the 'Everyone Else' scenario if the course is currentlySelected
else {
this.updateAssignment(assignment_id, {due_at: date, please_ignore: false, hadOriginalErrors: true})
}
},
assignmentOverrideOrigianlErrorCheck(a) {
a.hadOriginalErrors = a.hadOriginalErrors == true
},
saveAssignments () {
var assignments = assignmentUtils.withOriginalErrorsNotIgnored(this.getAssignments())
var course_id = this.getState().course.id
_.each(assignments, (a) => {
this.assignmentOverrideOrigianlErrorCheck(a)
assignmentUtils.saveAssignmentToCanvas(course_id, a)
});
},
postGrades() {
var assignments = assignmentUtils.notIgnored(this.getAssignments())
var selected = this.getState().selected
assignmentUtils.postGradesThroughCanvas(selected, assignments)
},
getPage () {
var state = this.getState()
if (state.pleaseShowNeedsGradingPage) {
return "needsGrading"
} else {
var originals = assignmentUtils.withOriginalErrors(this.getAssignments())
var withErrorsCount = _.keys(assignmentUtils.withErrors(this.getAssignments())).length
if (withErrorsCount == 0 && (state.pleaseShowSummaryPage || originals.length == 0)) {
return "summary"
} else {
return "corrections"
}
setGradeBookAssignments(gradebookAssignments) {
var assignments = []
for (var id in gradebookAssignments) {
var gba = gradebookAssignments[id]
// Only accept assignments suitable to post, e.g. published, post_to_sis
if (assignmentUtils.suitableToPost(gba)) {
// Push a copy, and only of relevant attributes
assignments.push(assignmentUtils.copyFromGradebook(gba))
}
}
})
// A second loop is needed to ensure non-unique name errors are included
// in hasError
_.each(assignments, a => {
a.original_error = assignmentUtils.hasError(assignments, a)
})
this.setState({assignments: assignments})
},
return store
};
setSections(sections) {
this.setState({sections: sections})
this.setSelectedSection(this.getState().sectionToShow)
},
validCheck(a) {
if (
a.overrideForThisSection != undefined &&
a.currentlySelected.type == 'course' &&
a.currentlySelected.id.toString() == a.overrideForThisSection.course_section_id
) {
return a.due_at != null ? true : false
} else if (
a.overrideForThisSection != undefined &&
a.currentlySelected.type == 'section' &&
a.currentlySelected.id.toString() == a.overrideForThisSection.course_section_id
) {
return a.overrideForThisSection.due_at != null ? true : false
} else {
return true
}
},
getAssignments() {
var assignments = this.getState().assignments
var state = this.getState()
if (state.selected.type == 'section') {
_.each(assignments, a => {
a.recentlyUpdated = false
a.currentlySelected = state.selected
a.sectionCount = _.keys(state.sections).length
a.overrideForThisSection = _.find(a.overrides, override => {
return override.course_section_id == state.selected.id
})
//Handle assignment with overrides and the 'Everyone Else' scenario with a section that does not have any overrides
//cleanup overrideForThisSection logic
if (a.overrideForThisSection == undefined) {
a.selectedSectionForEveryone = this.overrideForEveryone(a)
}
})
} else {
_.each(assignments, a => {
a.recentlyUpdated = false
a.currentlySelected = state.selected
a.sectionCount = _.keys(state.sections).length
//Course is currentlySlected with sections that have overrides AND are invalid
a.overrideForThisSection = _.find(a.overrides, override => {
return override.due_at == null || typeof override.due_at == 'object'
})
//Handle assignment with overrides and the 'Everyone Else' scenario with the course currentlySelected
if (a.overrideForThisSection == undefined) {
a.selectedSectionForEveryone = this.overrideForEveryone(a)
}
})
}
return assignments
},
getAssignment(assignment_id) {
var assignments = this.getAssignments()
return _.find(assignments, a => a.id == assignment_id)
},
setSelectedSection(section) {
var state = this.getState()
var section_id = parseInt(section)
var selected
if (section) {
selected = {
type: 'section',
id: section_id,
sis_id: this.getSISSectionId(section_id)
}
} else {
selected = {
type: 'course',
id: state.course.id,
sis_id: state.course.sis_id
}
}
this.setState({selected: selected, sectionToShow: section})
},
updateAssignment(assignment_id, newAttrs) {
var assignments = this.getAssignments()
var assignment = _.find(assignments, a => a.id == assignment_id)
$.extend(assignment, newAttrs)
this.setState({assignments: assignments})
},
updateAssignmentDate(assignment_id, date) {
var assignments = this.getState().assignments
var assignment = _.find(assignments, a => a.id == assignment_id)
//the assignment has an override and the override being updated is for the section that is currentlySelected update it
if (
assignment.overrideForThisSection != undefined &&
assignment.currentlySelected.id.toString() ==
assignment.overrideForThisSection.course_section_id
) {
assignment.overrideForThisSection.due_at = date
assignment.please_ignore = false
assignment.hadOriginalErrors = true
this.setState({assignments: assignments})
}
//the section override being set from the course level of the sction dropdown
else if (
assignment.overrideForThisSection != undefined &&
assignment.currentlySelected.id.toString() !=
assignment.overrideForThisSection.course_section_id
) {
assignment.overrideForThisSection.due_at = date
assignment.please_ignore = false
assignment.hadOriginalErrors = true
this.setState({assignments: assignments})
}
//update normal assignment and the 'Everyone Else' scenario if the course is currentlySelected
else {
this.updateAssignment(assignment_id, {
due_at: date,
please_ignore: false,
hadOriginalErrors: true
})
}
},
assignmentOverrideOrigianlErrorCheck(a) {
a.hadOriginalErrors = a.hadOriginalErrors == true
},
saveAssignments() {
var assignments = assignmentUtils.withOriginalErrorsNotIgnored(this.getAssignments())
var course_id = this.getState().course.id
_.each(assignments, a => {
this.assignmentOverrideOrigianlErrorCheck(a)
assignmentUtils.saveAssignmentToCanvas(course_id, a)
})
},
postGrades() {
var assignments = assignmentUtils.notIgnored(this.getAssignments())
var selected = this.getState().selected
assignmentUtils.postGradesThroughCanvas(selected, assignments)
},
getPage() {
var state = this.getState()
if (state.pleaseShowNeedsGradingPage) {
return 'needsGrading'
} else {
var originals = assignmentUtils.withOriginalErrors(this.getAssignments())
var withErrorsCount = _.keys(assignmentUtils.withErrors(this.getAssignments())).length
if (withErrorsCount == 0 && (state.pleaseShowSummaryPage || originals.length == 0)) {
return 'summary'
} else {
return 'corrections'
}
}
}
})
return store
}
export default PostGradesStore

View File

@ -20,230 +20,330 @@ import $ from 'jquery'
import _ from 'underscore'
import '../../shared/helpers/createStore'
let assignmentUtils = {
copyFromGradebook (assignment) {
var a = _.pick(assignment, [
"id",
"name",
"due_at",
"needs_grading_count",
"overrides"
])
a.please_ignore = false
a.original_error = false
return a
},
let assignmentUtils = {
copyFromGradebook(assignment) {
var a = _.pick(assignment, ['id', 'name', 'due_at', 'needs_grading_count', 'overrides'])
a.please_ignore = false
a.original_error = false
return a
},
namesMatch (a, b) {
return a.name === b.name && a !== b
},
namesMatch(a, b) {
return a.name === b.name && a !== b
},
nameTooLong (a) {
if (_.unescape(a.name).length > 30){
return true
}
else{
return false
}
},
nameEmpty (a) {
if (a.name.length == 0){
return true
}
else{
return false
}
},
notUniqueName (assignments, a) {
return assignments.some(_.partial(assignmentUtils.namesMatch, a))
},
noDueDateForEveryoneElseOverride(a) {
var has_overrides = a.overrides != undefined ? a.overrides.length > 0 : false
if(has_overrides && a.overrides.length != a.sectionCount && !a.due_at){
return true
} else {
return false
}
},
withOriginalErrors (assignments) {
// This logic handles an assignment with multiple overrides
// because #setGradeBookAssignments runs on load
// it does not have a reference to what the currently viewed section is.
// Due to this an assignment with 2 overrides (one valid, one invalid)
// it will set original_errors to true. This logic checks the override
// being viewed for that section. If the override is valid make
// original error false so that the override is not shown. Vice versa
// for the invalid override on the assignment.
_.each(assignments, (a) => {
if(a.overrideForThisSection != undefined && a.recentlyUpdated != undefined && a.recentlyUpdated == true && a.overrideForThisSection.due_at != null){a.original_error = false}
else if(a.overrideForThisSection != undefined && a.recentlyUpdated != undefined && a.recentlyUpdated == false && a.overrideForThisSection.due_at == null){a.original_error = true}
//for handling original error detection of a valid override for one section and an invalid override for another section
else if(a.overrideForThisSection != undefined && a.overrideForThisSection.due_at != null && !assignmentUtils.noDueDateForEveryoneElseOverride(a) && a.recentlyUpdated == false && a.hadOriginalErrors == false){a.original_error = false}
//for handling original error detection of a valid override for one section and the EveryoneElse "override" scenario
else if(a.overrideForThisSection != undefined && a.overrideForThisSection.due_at != null && assignmentUtils.noDueDateForEveryoneElseOverride(a) && a.currentlySelected.id.toString() == a.overrideForThisSection.course_section_id && a.recentlyUpdated == false && a.hadOriginalErrors == false){a.original_error = false}
//for handling original error detection of an override for one section and the EveryoneElse "override" scenario but the second section is currentlySelected and IS NOT valid
else if(a.overrideForThisSection == undefined && assignmentUtils.noDueDateForEveryoneElseOverride(a) && a.due_at == null && a.currentlySelected.id.toString() == a.selectedSectionForEveryone){a.original_error = true}
//for handling original error detection of an override for one section and the EveryoneElse "override" scenario but the second section is currentlySelected and IS valid
else if(a.overrideForThisSection == undefined && a.due_at != null && a.currentlySelected.id.toString() == a.selectedSectionForEveryone && a.hadOriginalErrors == false){a.original_error = false}
//for handling original error detection of an "override" in the 'EveryoneElse "override" scenario but the course is currentlySelected and IS NOT valid
else if(a.overrideForThisSection == undefined && assignmentUtils.noDueDateForEveryoneElseOverride(a) && a.due_at == null && a.currentlySelected.type == 'course' && a.currentlySelected.id.toString() != a.selectedSectionForEveryone){a.original_error = true}
//for handling original error detection of an "override" in the 'EveryoneElse "override" scenario but the course is currentlySelected and IS valid
else if(a.overrideForThisSection == undefined && a.due_at != null && a.currentlySelected.type == 'course' && a.currentlySelected.id.toString() != a.selectedSectionForEveryone && a.hadOriginalErrors == false){a.original_error = false}
});
return _.filter(assignments, (a) => a.original_error && !a.please_ignore)
},
withOriginalErrorsNotIgnored (assignments) {
return _.filter(assignments, function(a){ return (a.original_error || a.hadOriginalErrors) && !a.please_ignore})
},
withErrors (assignments) {
return _.filter(assignments, (a) => assignmentUtils.hasError(assignments, a))
},
notIgnored (assignments) {
return _.filter(assignments, (a) => !a.please_ignore)
},
needsGrading (assignments) {
return _.filter(assignments, (a) => a.needs_grading_count > 0)
},
hasError (assignments, a) {
////Decided to ignore
if(a.please_ignore) return false
////Not unique
if(assignmentUtils.notUniqueName(assignments, a)) return true
////Name too long
if(assignmentUtils.nameTooLong(a)) return true
////Name empty
if(assignmentUtils.nameEmpty(a)) return true
////Non-override missing due_at
var has_overrides = a.overrides != undefined ? a.overrides.length > 0 : false
if(!has_overrides && !a.due_at) return true
////Override missing due_at
var has_this_override = a.overrideForThisSection != undefined
if(has_this_override && a.overrideForThisSection.due_at == null && a.currentlySelected.id.toString() == a.overrideForThisSection.course_section_id) return true
////Override missing due_at while currentlySelecteed is at the course level
if(has_this_override && a.overrideForThisSection.due_at == null && a.currentlySelected.id.toString() != a.overrideForThisSection.course_section_id) return true
////Has one override and another override for 'Everyone Else'
////
////The override for 'Everyone Else' isn't really an override and references
////the assignments actual due_at. So we must check for this behavior
if(assignmentUtils.noDueDateForEveryoneElseOverride(a) && a.currentlySelected != undefined && a.overrideForThisSection != undefined && a.currentlySelected.id.toString() != a.overrideForThisSection.course_section_id) return true
////Has only one override but the section that is currently selected does not have an override thus causing the assignment to have due_at that is null making it invalid
if(assignmentUtils.noDueDateForEveryoneElseOverride(a) && a.overrideForThisSection == undefined && a.currentlySelected != undefined && a.currentlySelected.id.toString() == a.selectedSectionForEveryone) return true
////'Everyone Else' scenario and the course is currentlySelected but due_at is null making it invalid
if(assignmentUtils.noDueDateForEveryoneElseOverride(a) && a.overrideForThisSection == undefined && a.currentlySelected != undefined && a.currentlySelected.type == 'course' && a.currentlySelected.id.toString() != a.selectedSectionForEveryone) return true
////Passes all tests, looks good.
nameTooLong(a) {
if (_.unescape(a.name).length > 30) {
return true
} else {
return false
},
}
},
suitableToPost(assignment) {
return assignment.published && assignment.post_to_sis
},
nameEmpty(a) {
if (a.name.length == 0) {
return true
} else {
return false
}
},
saveAssignmentToCanvas (course_id, assignment) {
// if the date on an override is being updated confirm by checking if the due_at is an object
if(assignment.overrideForThisSection != undefined && typeof(assignment.overrideForThisSection.due_at) == "object") {
//allows the validation process to determine when it has been updated and can display the correct page
assignment.hadOriginalErrors = false
var url = '/api/v1/courses/' + course_id + '/assignments/' + assignment.id + '/overrides/' + assignment.overrideForThisSection.id
//sets up form data to allow a single override to be updated
var fd = new FormData();
fd.append( 'assignment_override[due_at]', assignment.overrideForThisSection.due_at.toISOString() )
notUniqueName(assignments, a) {
return assignments.some(_.partial(assignmentUtils.namesMatch, a))
},
$.ajax(url, {
type: 'PUT',
data: fd,
processData: false,
contentType: false,
error: (err) => {
var msg = 'An error occurred saving assignment override, (' + assignment.overrideForThisSection.id + '). '
msg += "HTTP Error " + data.status + " : " + data.statusText
$.flashError(msg)
}
})
// if there is a naming conflict on the assignment that has an override with a date
// that was just set AND the naming conflict is fixed we must also update the assignment
// to mock natural behavior to the user so that the naming conflict does not appear again
url = '/api/v1/courses/' + course_id + '/assignments/' + assignment.id
data = { assignment: {
name: assignment.name,
due_at: assignment.due_at
}}
$.ajax(url, {
type: 'PUT',
data: JSON.stringify(data),
contentType: 'application/json; charset=utf-8',
error: (err) => {
var msg = 'An error occurred saving assignment (' + assignment.id + '). '
msg += "HTTP Error " + data.status + " : " + data.statusText
$.flashError(msg)
}
})
noDueDateForEveryoneElseOverride(a) {
var has_overrides = a.overrides != undefined ? a.overrides.length > 0 : false
if (has_overrides && a.overrides.length != a.sectionCount && !a.due_at) {
return true
} else {
return false
}
},
withOriginalErrors(assignments) {
// This logic handles an assignment with multiple overrides
// because #setGradeBookAssignments runs on load
// it does not have a reference to what the currently viewed section is.
// Due to this an assignment with 2 overrides (one valid, one invalid)
// it will set original_errors to true. This logic checks the override
// being viewed for that section. If the override is valid make
// original error false so that the override is not shown. Vice versa
// for the invalid override on the assignment.
_.each(assignments, a => {
if (
a.overrideForThisSection != undefined &&
a.recentlyUpdated != undefined &&
a.recentlyUpdated == true &&
a.overrideForThisSection.due_at != null
) {
a.original_error = false
} else if (
a.overrideForThisSection != undefined &&
a.recentlyUpdated != undefined &&
a.recentlyUpdated == false &&
a.overrideForThisSection.due_at == null
) {
a.original_error = true
}
else {
//allows the validation process to determine when it has been updated and can display the correct page
assignment.hadOriginalErrors = false
var url = '/api/v1/courses/' + course_id + '/assignments/' + assignment.id
var data = { assignment: {
name: assignment.name,
due_at: assignment.due_at
}}
$.ajax(url, {
type: 'PUT',
data: JSON.stringify(data),
contentType: 'application/json; charset=utf-8',
error: (err) => {
var msg = 'An error occurred saving assignment (' + assignment.id + '). '
msg += "HTTP Error " + data.status + " : " + data.statusText
$.flashError(msg)
}
})
//for handling original error detection of a valid override for one section and an invalid override for another section
else if (
a.overrideForThisSection != undefined &&
a.overrideForThisSection.due_at != null &&
!assignmentUtils.noDueDateForEveryoneElseOverride(a) &&
a.recentlyUpdated == false &&
a.hadOriginalErrors == false
) {
a.original_error = false
}
//for handling original error detection of a valid override for one section and the EveryoneElse "override" scenario
else if (
a.overrideForThisSection != undefined &&
a.overrideForThisSection.due_at != null &&
assignmentUtils.noDueDateForEveryoneElseOverride(a) &&
a.currentlySelected.id.toString() == a.overrideForThisSection.course_section_id &&
a.recentlyUpdated == false &&
a.hadOriginalErrors == false
) {
a.original_error = false
}
//for handling original error detection of an override for one section and the EveryoneElse "override" scenario but the second section is currentlySelected and IS NOT valid
else if (
a.overrideForThisSection == undefined &&
assignmentUtils.noDueDateForEveryoneElseOverride(a) &&
a.due_at == null &&
a.currentlySelected.id.toString() == a.selectedSectionForEveryone
) {
a.original_error = true
}
//for handling original error detection of an override for one section and the EveryoneElse "override" scenario but the second section is currentlySelected and IS valid
else if (
a.overrideForThisSection == undefined &&
a.due_at != null &&
a.currentlySelected.id.toString() == a.selectedSectionForEveryone &&
a.hadOriginalErrors == false
) {
a.original_error = false
}
//for handling original error detection of an "override" in the 'EveryoneElse "override" scenario but the course is currentlySelected and IS NOT valid
else if (
a.overrideForThisSection == undefined &&
assignmentUtils.noDueDateForEveryoneElseOverride(a) &&
a.due_at == null &&
a.currentlySelected.type == 'course' &&
a.currentlySelected.id.toString() != a.selectedSectionForEveryone
) {
a.original_error = true
}
//for handling original error detection of an "override" in the 'EveryoneElse "override" scenario but the course is currentlySelected and IS valid
else if (
a.overrideForThisSection == undefined &&
a.due_at != null &&
a.currentlySelected.type == 'course' &&
a.currentlySelected.id.toString() != a.selectedSectionForEveryone &&
a.hadOriginalErrors == false
) {
a.original_error = false
}
})
return _.filter(assignments, a => a.original_error && !a.please_ignore)
},
},
withOriginalErrorsNotIgnored(assignments) {
return _.filter(assignments, function(a) {
return (a.original_error || a.hadOriginalErrors) && !a.please_ignore
})
},
withErrors(assignments) {
return _.filter(assignments, a => assignmentUtils.hasError(assignments, a))
},
notIgnored(assignments) {
return _.filter(assignments, a => !a.please_ignore)
},
needsGrading(assignments) {
return _.filter(assignments, a => a.needs_grading_count > 0)
},
hasError(assignments, a) {
////Decided to ignore
if (a.please_ignore) return false
////Not unique
if (assignmentUtils.notUniqueName(assignments, a)) return true
////Name too long
if (assignmentUtils.nameTooLong(a)) return true
////Name empty
if (assignmentUtils.nameEmpty(a)) return true
////Non-override missing due_at
var has_overrides = a.overrides != undefined ? a.overrides.length > 0 : false
if (!has_overrides && !a.due_at) return true
////Override missing due_at
var has_this_override = a.overrideForThisSection != undefined
if (
has_this_override &&
a.overrideForThisSection.due_at == null &&
a.currentlySelected.id.toString() == a.overrideForThisSection.course_section_id
)
return true
////Override missing due_at while currentlySelecteed is at the course level
if (
has_this_override &&
a.overrideForThisSection.due_at == null &&
a.currentlySelected.id.toString() != a.overrideForThisSection.course_section_id
)
return true
////Has one override and another override for 'Everyone Else'
////
////The override for 'Everyone Else' isn't really an override and references
////the assignments actual due_at. So we must check for this behavior
if (
assignmentUtils.noDueDateForEveryoneElseOverride(a) &&
a.currentlySelected != undefined &&
a.overrideForThisSection != undefined &&
a.currentlySelected.id.toString() != a.overrideForThisSection.course_section_id
)
return true
////Has only one override but the section that is currently selected does not have an override thus causing the assignment to have due_at that is null making it invalid
if (
assignmentUtils.noDueDateForEveryoneElseOverride(a) &&
a.overrideForThisSection == undefined &&
a.currentlySelected != undefined &&
a.currentlySelected.id.toString() == a.selectedSectionForEveryone
)
return true
////'Everyone Else' scenario and the course is currentlySelected but due_at is null making it invalid
if (
assignmentUtils.noDueDateForEveryoneElseOverride(a) &&
a.overrideForThisSection == undefined &&
a.currentlySelected != undefined &&
a.currentlySelected.type == 'course' &&
a.currentlySelected.id.toString() != a.selectedSectionForEveryone
)
return true
////Passes all tests, looks good.
return false
},
suitableToPost(assignment) {
return assignment.published && assignment.post_to_sis
},
saveAssignmentToCanvas(course_id, assignment) {
// if the date on an override is being updated confirm by checking if the due_at is an object
if (
assignment.overrideForThisSection != undefined &&
typeof assignment.overrideForThisSection.due_at == 'object'
) {
//allows the validation process to determine when it has been updated and can display the correct page
assignment.hadOriginalErrors = false
var url =
'/api/v1/courses/' +
course_id +
'/assignments/' +
assignment.id +
'/overrides/' +
assignment.overrideForThisSection.id
//sets up form data to allow a single override to be updated
var fd = new FormData()
fd.append(
'assignment_override[due_at]',
assignment.overrideForThisSection.due_at.toISOString()
)
// Sends a post-grades request to Canvas that is then forwarded to SIS App.
// Expects a list of assignments that will later be queried for grades via
// SIS App's workers
postGradesThroughCanvas (selected, assignments) {
var url = "/api/v1/" + selected.type + "s/" + selected.id + "/post_grades/"
var data = { assignments: _.map(assignments, (assignment) => assignment.id) }
$.ajax(url, {
type: 'POST',
type: 'PUT',
data: fd,
processData: false,
contentType: false,
error: err => {
var msg =
'An error occurred saving assignment override, (' +
assignment.overrideForThisSection.id +
'). '
msg += 'HTTP Error ' + data.status + ' : ' + data.statusText
$.flashError(msg)
}
})
// if there is a naming conflict on the assignment that has an override with a date
// that was just set AND the naming conflict is fixed we must also update the assignment
// to mock natural behavior to the user so that the naming conflict does not appear again
url = '/api/v1/courses/' + course_id + '/assignments/' + assignment.id
data = {
assignment: {
name: assignment.name,
due_at: assignment.due_at
}
}
$.ajax(url, {
type: 'PUT',
data: JSON.stringify(data),
contentType: 'application/json; charset=utf-8',
success: (msg) =>{
if (msg.error){
$.flashError(msg.error)
}else{
$.flashMessage(msg.message)
}
},
error: (err) => {
var msg = 'An error occurred posting grades for (' + selected.type + ' : ' + selected.id +'). '
msg += "HTTP Error " + data.status + " : " + data.statusText
error: err => {
var msg = 'An error occurred saving assignment (' + assignment.id + '). '
msg += 'HTTP Error ' + data.status + ' : ' + data.statusText
$.flashError(msg)
}
})
} else {
//allows the validation process to determine when it has been updated and can display the correct page
assignment.hadOriginalErrors = false
var url = '/api/v1/courses/' + course_id + '/assignments/' + assignment.id
var data = {
assignment: {
name: assignment.name,
due_at: assignment.due_at
}
}
$.ajax(url, {
type: 'PUT',
data: JSON.stringify(data),
contentType: 'application/json; charset=utf-8',
error: err => {
var msg = 'An error occurred saving assignment (' + assignment.id + '). '
msg += 'HTTP Error ' + data.status + ' : ' + data.statusText
$.flashError(msg)
}
})
}
},
};
// Sends a post-grades request to Canvas that is then forwarded to SIS App.
// Expects a list of assignments that will later be queried for grades via
// SIS App's workers
postGradesThroughCanvas(selected, assignments) {
var url = '/api/v1/' + selected.type + 's/' + selected.id + '/post_grades/'
var data = {assignments: _.map(assignments, assignment => assignment.id)}
$.ajax(url, {
type: 'POST',
data: JSON.stringify(data),
contentType: 'application/json; charset=utf-8',
success: msg => {
if (msg.error) {
$.flashError(msg.error)
} else {
$.flashMessage(msg.message)
}
},
error: err => {
var msg =
'An error occurred posting grades for (' + selected.type + ' : ' + selected.id + '). '
msg += 'HTTP Error ' + data.status + ' : ' + data.statusText
$.flashError(msg)
}
})
}
}
export default assignmentUtils

View File

@ -19,100 +19,117 @@
import _ from 'underscore'
import GradingPeriodsHelper from '../grading/helpers/GradingPeriodsHelper'
const TOOLTIP_KEYS = {
NOT_IN_ANY_GP: "not_in_any_grading_period",
IN_ANOTHER_GP: "in_another_grading_period",
IN_CLOSED_GP: "in_closed_grading_period",
NONE: null
};
const TOOLTIP_KEYS = {
NOT_IN_ANY_GP: 'not_in_any_grading_period',
IN_ANOTHER_GP: 'in_another_grading_period',
IN_CLOSED_GP: 'in_closed_grading_period',
NONE: null
}
function visibleToStudent(assignment, student) {
if (!assignment.only_visible_to_overrides) return true;
return _.contains(assignment.assignment_visibility, student.id);
function visibleToStudent(assignment, student) {
if (!assignment.only_visible_to_overrides) return true
return _.contains(assignment.assignment_visibility, student.id)
}
function cellMapForSubmission(
assignment,
student,
hasGradingPeriods,
selectedGradingPeriodID,
isAdmin
) {
if (assignment.moderated_grading && !assignment.grades_published) {
return {locked: true, hideGrade: false}
} else if (assignment.anonymize_students) {
return {locked: true, hideGrade: true}
} else if (!visibleToStudent(assignment, student)) {
return {locked: true, hideGrade: true, tooltip: TOOLTIP_KEYS.NONE}
} else if (hasGradingPeriods) {
return cellMappingsForMultipleGradingPeriods(
assignment,
student,
selectedGradingPeriodID,
isAdmin
)
} else {
return {locked: false, hideGrade: false, tooltip: TOOLTIP_KEYS.NONE}
}
}
function cellMapForSubmission(
function submissionGradingPeriodInformation(assignment, student) {
const submissionInfo = assignment.effectiveDueDates[student.id] || {}
return {
gradingPeriodID: submissionInfo.grading_period_id,
inClosedGradingPeriod: submissionInfo.in_closed_grading_period
}
}
function cellMappingsForMultipleGradingPeriods(
assignment,
student,
selectedGradingPeriodID,
isAdmin
) {
const specificPeriodSelected = !GradingPeriodsHelper.isAllGradingPeriods(selectedGradingPeriodID)
const {gradingPeriodID, inClosedGradingPeriod} = submissionGradingPeriodInformation(
assignment,
student
)
if (specificPeriodSelected && !gradingPeriodID) {
return {locked: true, hideGrade: true, tooltip: TOOLTIP_KEYS.NOT_IN_ANY_GP}
} else if (specificPeriodSelected && selectedGradingPeriodID != gradingPeriodID) {
return {locked: true, hideGrade: true, tooltip: TOOLTIP_KEYS.IN_ANOTHER_GP}
} else if (!isAdmin && inClosedGradingPeriod) {
return {locked: true, hideGrade: false, tooltip: TOOLTIP_KEYS.IN_CLOSED_GP}
} else {
return {locked: false, hideGrade: false, tooltip: TOOLTIP_KEYS.NONE}
}
}
class SubmissionState {
constructor({hasGradingPeriods, selectedGradingPeriodID, isAdmin}) {
this.hasGradingPeriods = hasGradingPeriods
this.selectedGradingPeriodID = selectedGradingPeriodID
this.isAdmin = isAdmin
this.submissionCellMap = {}
this.submissionMap = {}
}
setup(students, assignments) {
students.forEach(student => {
this.submissionCellMap[student.id] = {}
this.submissionMap[student.id] = {}
_.each(assignments, assignment => {
this.setSubmissionCellState(student, assignment, student[`assignment_${assignment.id}`])
})
})
}
setSubmissionCellState(
student,
hasGradingPeriods,
selectedGradingPeriodID,
isAdmin
assignment,
submission = {assignment_id: assignment.id, user_id: student.id}
) {
if (assignment.moderated_grading && !assignment.grades_published) {
return { locked: true, hideGrade: false };
} else if (assignment.anonymize_students) {
return { locked: true, hideGrade: true };
} else if (!visibleToStudent(assignment, student)) {
return { locked: true, hideGrade: true, tooltip: TOOLTIP_KEYS.NONE };
} else if (hasGradingPeriods) {
return cellMappingsForMultipleGradingPeriods(assignment, student, selectedGradingPeriodID, isAdmin);
} else {
return { locked: false, hideGrade: false, tooltip: TOOLTIP_KEYS.NONE };
}
this.submissionMap[student.id][assignment.id] = submission
const params = [
assignment,
student,
this.hasGradingPeriods,
this.selectedGradingPeriodID,
this.isAdmin
]
this.submissionCellMap[student.id][assignment.id] = cellMapForSubmission(...params)
}
function submissionGradingPeriodInformation(assignment, student) {
const submissionInfo = assignment.effectiveDueDates[student.id] || {};
return {
gradingPeriodID: submissionInfo.grading_period_id,
inClosedGradingPeriod: submissionInfo.in_closed_grading_period
};
getSubmission(user_id, assignment_id) {
return (this.submissionMap[user_id] || {})[assignment_id]
}
function cellMappingsForMultipleGradingPeriods(assignment, student, selectedGradingPeriodID, isAdmin) {
const specificPeriodSelected = !GradingPeriodsHelper.isAllGradingPeriods(selectedGradingPeriodID);
const { gradingPeriodID, inClosedGradingPeriod } = submissionGradingPeriodInformation(assignment, student);
if (specificPeriodSelected && !gradingPeriodID) {
return { locked: true, hideGrade: true, tooltip: TOOLTIP_KEYS.NOT_IN_ANY_GP };
} else if (specificPeriodSelected && selectedGradingPeriodID != gradingPeriodID) {
return { locked: true, hideGrade: true, tooltip: TOOLTIP_KEYS.IN_ANOTHER_GP };
} else if (!isAdmin && inClosedGradingPeriod) {
return { locked: true, hideGrade: false, tooltip: TOOLTIP_KEYS.IN_CLOSED_GP };
} else {
return { locked: false, hideGrade: false, tooltip: TOOLTIP_KEYS.NONE };
}
getSubmissionState({user_id, assignment_id}) {
return (this.submissionCellMap[user_id] || {})[assignment_id]
}
class SubmissionState {
constructor({ hasGradingPeriods, selectedGradingPeriodID, isAdmin }) {
this.hasGradingPeriods = hasGradingPeriods;
this.selectedGradingPeriodID = selectedGradingPeriodID;
this.isAdmin = isAdmin;
this.submissionCellMap = {};
this.submissionMap = {};
}
setup(students, assignments) {
students.forEach((student) => {
this.submissionCellMap[student.id] = {};
this.submissionMap[student.id] = {};
_.each(assignments, (assignment) => {
this.setSubmissionCellState(student, assignment, student[`assignment_${assignment.id}`]);
});
});
}
setSubmissionCellState(student, assignment, submission = { assignment_id: assignment.id, user_id: student.id }) {
this.submissionMap[student.id][assignment.id] = submission;
const params = [
assignment,
student,
this.hasGradingPeriods,
this.selectedGradingPeriodID,
this.isAdmin
];
this.submissionCellMap[student.id][assignment.id] = cellMapForSubmission(...params);
}
getSubmission(user_id, assignment_id) {
return (this.submissionMap[user_id] || {})[assignment_id];
}
getSubmissionState({ user_id, assignment_id }) {
return (this.submissionCellMap[user_id] || {})[assignment_id];
}
};
}
export default SubmissionState

View File

@ -16,8 +16,8 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const constants = {
MAX_NOTE_LENGTH: 255
};
const constants = {
MAX_NOTE_LENGTH: 255
}
export default constants

View File

@ -50,7 +50,7 @@ export function sumBy(collection, attr) {
}
export function scoreToPercentage(score, pointsPossible) {
const floatingPointResult = score / pointsPossible * 100
const floatingPointResult = (score / pointsPossible) * 100
if (!Number.isFinite(floatingPointResult)) {
return floatingPointResult
}
@ -59,5 +59,9 @@ export function scoreToPercentage(score, pointsPossible) {
}
export function weightedPercent({score, possible, weight}) {
return (score && weight) ? Big(score).div(possible).times(weight) : Big(0)
return score && weight
? Big(score)
.div(possible)
.times(weight)
: Big(0)
}

View File

@ -22,89 +22,93 @@ import numberHelper from '../../../shared/helpers/numberHelper'
import {scoreToPercentage} from './GradeCalculationHelper'
import {scoreToGrade} from '../../../gradebook/GradingSchemeHelper'
const POINTS = 'points';
const PERCENT = 'percent';
const PASS_FAIL = 'pass_fail';
const POINTS = 'points'
const PERCENT = 'percent'
const PASS_FAIL = 'pass_fail'
const PASS_GRADES = ['complete', 'pass'];
const FAIL_GRADES = ['incomplete', 'fail'];
const PASS_GRADES = ['complete', 'pass']
const FAIL_GRADES = ['incomplete', 'fail']
const UNGRADED = ''
function isPassFail (grade, gradeType) {
function isPassFail(grade, gradeType) {
if (gradeType) {
return gradeType === PASS_FAIL;
return gradeType === PASS_FAIL
}
return PASS_GRADES.includes(grade) || FAIL_GRADES.includes(grade);
return PASS_GRADES.includes(grade) || FAIL_GRADES.includes(grade)
}
function isPercent (grade, gradeType) {
function isPercent(grade, gradeType) {
if (gradeType) {
return gradeType === PERCENT;
return gradeType === PERCENT
}
return /%/g.test(grade);
return /%/g.test(grade)
}
function isExcused (grade) {
return grade === 'EX';
function isExcused(grade) {
return grade === 'EX'
}
function normalizeCompleteIncompleteGrade (grade) {
function normalizeCompleteIncompleteGrade(grade) {
if (PASS_GRADES.includes(grade)) {
return 'complete';
return 'complete'
}
if (FAIL_GRADES.includes(grade)) {
return 'incomplete';
return 'incomplete'
}
return null;
return null
}
function shouldFormatGradingType (gradingType) {
return gradingType === POINTS || gradingType === PERCENT || gradingType === PASS_FAIL;
function shouldFormatGradingType(gradingType) {
return gradingType === POINTS || gradingType === PERCENT || gradingType === PASS_FAIL
}
function shouldFormatGrade (grade, gradingType) {
function shouldFormatGrade(grade, gradingType) {
if (gradingType) {
return shouldFormatGradingType(gradingType);
return shouldFormatGradingType(gradingType)
}
return typeof grade === 'number' || isPassFail(grade);
return typeof grade === 'number' || isPassFail(grade)
}
function excused () {
return I18n.t('Excused');
function excused() {
return I18n.t('Excused')
}
function formatPointsGrade (score) {
return I18n.n(score, { precision: 2, strip_insignificant_zeros: true });
function formatPointsGrade(score) {
return I18n.n(score, {precision: 2, strip_insignificant_zeros: true})
}
function formatPercentageGrade (score, options) {
function formatPercentageGrade(score, options) {
const percent = options.pointsPossible ? scoreToPercentage(score, options.pointsPossible) : score
return I18n.n(round(percent, 2), { percentage: true, precision: 2, strip_insignificant_zeros: true });
return I18n.n(round(percent, 2), {
percentage: true,
precision: 2,
strip_insignificant_zeros: true
})
}
function formatGradingSchemeGrade (score, grade, options) {
function formatGradingSchemeGrade(score, grade, options) {
if (options.pointsPossible) {
const percent = scoreToPercentage(score, options.pointsPossible)
return scoreToGrade(percent, options.gradingScheme);
return scoreToGrade(percent, options.gradingScheme)
} else if (grade != null) {
return grade;
return grade
} else {
return scoreToGrade(score, options.gradingScheme);
return scoreToGrade(score, options.gradingScheme)
}
}
function formatCompleteIncompleteGrade (score, grade, options) {
let passed = false;
function formatCompleteIncompleteGrade(score, grade, options) {
let passed = false
if (options.pointsPossible) {
passed = score > 0;
passed = score > 0
} else {
passed = PASS_GRADES.includes(grade);
passed = PASS_GRADES.includes(grade)
}
return passed ? I18n.t('Complete') : I18n.t('Incomplete');
return passed ? I18n.t('Complete') : I18n.t('Incomplete')
}
function formatGradeInfo(gradeInfo, options = {}) {
@ -137,30 +141,30 @@ const GradeFormatHelper = {
* @return {string} Given grade rounded to two decimal places and formatted with I18n
* if it is a point or percent grade.
*/
formatGrade (grade, options = {}) {
let formattedGrade = grade;
formatGrade(grade, options = {}) {
let formattedGrade = grade
if (grade == null || grade === '') {
return ('defaultValue' in options) ? options.defaultValue : grade;
return 'defaultValue' in options ? options.defaultValue : grade
}
if (isExcused(grade)) {
return excused();
return excused()
}
let parsedGrade = GradeFormatHelper.parseGrade(grade, options);
let parsedGrade = GradeFormatHelper.parseGrade(grade, options)
if (shouldFormatGrade(parsedGrade, options.gradingType)) {
if (isPassFail(parsedGrade, options.gradingType)) {
parsedGrade = normalizeCompleteIncompleteGrade(parsedGrade);
formattedGrade = parsedGrade === 'complete' ? I18n.t('complete') : I18n.t('incomplete');
parsedGrade = normalizeCompleteIncompleteGrade(parsedGrade)
formattedGrade = parsedGrade === 'complete' ? I18n.t('complete') : I18n.t('incomplete')
} else {
const roundedGrade = round(parsedGrade, options.precision || 2);
formattedGrade = I18n.n(roundedGrade, { percentage: isPercent(grade, options.gradingType) });
const roundedGrade = round(parsedGrade, options.precision || 2)
formattedGrade = I18n.n(roundedGrade, {percentage: isPercent(grade, options.gradingType)})
}
}
return formattedGrade;
return formattedGrade
},
/**
@ -168,54 +172,56 @@ const GradeFormatHelper = {
* returns delocalized point or percentage string.
* Otherwise, returns input.
*/
delocalizeGrade (localizedGrade) {
if (localizedGrade === undefined ||
localizedGrade === null ||
typeof localizedGrade !== 'string') {
return localizedGrade;
delocalizeGrade(localizedGrade) {
if (
localizedGrade === undefined ||
localizedGrade === null ||
typeof localizedGrade !== 'string'
) {
return localizedGrade
}
const delocalizedGrade = numberHelper.parse(localizedGrade.replace('%', ''));
const delocalizedGrade = numberHelper.parse(localizedGrade.replace('%', ''))
if (isNaN(delocalizedGrade)) {
return localizedGrade;
return localizedGrade
}
return delocalizedGrade + (/%/g.test(localizedGrade) ? '%' : '');
return delocalizedGrade + (/%/g.test(localizedGrade) ? '%' : '')
},
parseGrade (grade, options = {}) {
let parsedGrade;
parseGrade(grade, options = {}) {
let parsedGrade
if (grade == null || grade === '' || typeof grade === 'number') {
return grade;
return grade
}
const gradeNoPercent = grade.replace('%', '')
if ( 'delocalize' in options && !options.delocalize && !isNaN(gradeNoPercent) ) {
parsedGrade = parseFloat(gradeNoPercent);
if ('delocalize' in options && !options.delocalize && !isNaN(gradeNoPercent)) {
parsedGrade = parseFloat(gradeNoPercent)
} else {
parsedGrade = numberHelper.parse(gradeNoPercent);
parsedGrade = numberHelper.parse(gradeNoPercent)
}
if (isNaN(parsedGrade)) {
return grade;
return grade
}
return parsedGrade;
return parsedGrade
},
excused,
isExcused,
formatGradeInfo,
formatSubmissionGrade (submission, options = { version: 'final' }) {
formatSubmissionGrade(submission, options = {version: 'final'}) {
if (submission.excused) {
return excused();
return excused()
}
const score = options.version === 'entered' ? submission.enteredScore : submission.score;
const grade = options.version === 'entered' ? submission.enteredGrade : submission.grade;
const score = options.version === 'entered' ? submission.enteredScore : submission.score
const grade = options.version === 'entered' ? submission.enteredGrade : submission.grade
if (score == null) {
return options.defaultValue != null ? options.defaultValue : UNGRADED
@ -223,17 +229,17 @@ const GradeFormatHelper = {
switch (options.formatType) {
case 'percent':
return formatPercentageGrade(score, options);
return formatPercentageGrade(score, options)
case 'gradingScheme':
return formatGradingSchemeGrade(score, grade, options);
return formatGradingSchemeGrade(score, grade, options)
case 'passFail':
return formatCompleteIncompleteGrade(score, grade, options);
return formatCompleteIncompleteGrade(score, grade, options)
default:
return formatPointsGrade(score);
return formatPointsGrade(score)
}
},
UNGRADED
};
}
export default GradeFormatHelper

View File

@ -16,50 +16,50 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import GradeFormatHelper from '../../../gradebook/shared/helpers/GradeFormatHelper';
import GradeFormatHelper from '../../../gradebook/shared/helpers/GradeFormatHelper'
const DEFAULT_GRADE = '';
const DEFAULT_GRADE = ''
export default {
scoreToGrade (score, assignment, gradingScheme) {
scoreToGrade(score, assignment, gradingScheme) {
if (score == null) {
return DEFAULT_GRADE;
return DEFAULT_GRADE
}
const gradingType = assignment.grading_type;
const gradingType = assignment.grading_type
switch (gradingType) {
case 'points': {
return GradeFormatHelper.formatGrade(score);
return GradeFormatHelper.formatGrade(score)
}
case 'percent': {
if (!assignment.points_possible) {
return DEFAULT_GRADE;
return DEFAULT_GRADE
}
const percentage = 100 * (score / assignment.points_possible);
return GradeFormatHelper.formatGrade(percentage, { gradingType });
const percentage = 100 * (score / assignment.points_possible)
return GradeFormatHelper.formatGrade(percentage, {gradingType})
}
case 'pass_fail': {
const grade = score ? 'complete' : 'incomplete';
return GradeFormatHelper.formatGrade(grade, { gradingType });
const grade = score ? 'complete' : 'incomplete'
return GradeFormatHelper.formatGrade(grade, {gradingType})
}
case 'letter_grade': {
if (!gradingScheme || !assignment.points_possible) {
return DEFAULT_GRADE;
return DEFAULT_GRADE
}
const normalizedScore = score / assignment.points_possible;
const normalizedScore = score / assignment.points_possible
for (let i = 0; i < gradingScheme.length; i += 1) {
if (gradingScheme[i][1] <= normalizedScore) {
return gradingScheme[i][0];
return gradingScheme[i][0]
}
}
}
// fall through
default:
return score;
return score
}
}
}

View File

@ -17,71 +17,83 @@
*/
import _ from 'underscore'
function uniqueEffectiveDueDates(assignment) {
const dueDates = _.map(assignment.effectiveDueDates, function(dueDateInfo) {
const dueAt = dueDateInfo.due_at;
return dueAt ? new Date(dueAt) : dueAt;
});
function uniqueEffectiveDueDates(assignment) {
const dueDates = _.map(assignment.effectiveDueDates, function(dueDateInfo) {
const dueAt = dueDateInfo.due_at
return dueAt ? new Date(dueAt) : dueAt
})
return _.uniq(dueDates, date => date ? date.toString() : date);
return _.uniq(dueDates, date => (date ? date.toString() : date))
}
function getDueDateFromAssignment(assignment) {
if (assignment.due_at) {
return new Date(assignment.due_at)
}
function getDueDateFromAssignment(assignment) {
if (assignment.due_at) {
return new Date(assignment.due_at);
}
const dueDates = uniqueEffectiveDueDates(assignment)
return dueDates.length === 1 ? dueDates[0] : null
}
const dueDates = uniqueEffectiveDueDates(assignment);
return dueDates.length === 1 ? dueDates[0] : null;
const assignmentHelper = {
compareByDueDate(a, b) {
let aDate = getDueDateFromAssignment(a)
let bDate = getDueDateFromAssignment(b)
const aDateIsNull = _.isNull(aDate)
const bDateIsNull = _.isNull(bDate)
if (aDateIsNull && !bDateIsNull) {
return 1
}
if (!aDateIsNull && bDateIsNull) {
return -1
}
if (aDateIsNull && bDateIsNull) {
const aHasMultipleDates = this.hasMultipleDueDates(a)
const bHasMultipleDates = this.hasMultipleDueDates(b)
if (aHasMultipleDates && !bHasMultipleDates) {
return -1
}
if (!aHasMultipleDates && bHasMultipleDates) {
return 1
}
}
aDate = +aDate
bDate = +bDate
if (aDate === bDate) {
const aName = a.name.toLowerCase()
const bName = b.name.toLowerCase()
if (aName === bName) {
return 0
}
return aName > bName ? 1 : -1
}
return aDate - bDate
},
hasMultipleDueDates(assignment) {
return uniqueEffectiveDueDates(assignment).length > 1
},
getComparator(arrangeBy) {
if (arrangeBy === 'due_date') {
return this.compareByDueDate.bind(this)
}
if (arrangeBy === 'assignment_group') {
return this.compareByAssignmentGroup.bind(this)
}
},
compareByAssignmentGroup(a, b) {
const diffOfAssignmentGroupPosition = a.assignment_group_position - b.assignment_group_position
if (diffOfAssignmentGroupPosition === 0) {
const diffOfAssignmentPosition = a.position - b.position
if (diffOfAssignmentPosition === 0) {
return 0
}
return diffOfAssignmentPosition
}
return diffOfAssignmentGroupPosition
}
const assignmentHelper = {
compareByDueDate (a, b) {
let aDate = getDueDateFromAssignment(a);
let bDate = getDueDateFromAssignment(b);
const aDateIsNull = _.isNull(aDate);
const bDateIsNull = _.isNull(bDate);
if (aDateIsNull && !bDateIsNull) { return 1 }
if (!aDateIsNull && bDateIsNull) { return -1 }
if (aDateIsNull && bDateIsNull) {
const aHasMultipleDates = this.hasMultipleDueDates(a);
const bHasMultipleDates = this.hasMultipleDueDates(b);
if (aHasMultipleDates && !bHasMultipleDates) { return -1 }
if (!aHasMultipleDates && bHasMultipleDates) { return 1 }
}
aDate = +aDate;
bDate = +bDate;
if (aDate === bDate) {
const aName = a.name.toLowerCase();
const bName = b.name.toLowerCase();
if (aName === bName) { return 0 }
return aName > bName ? 1 : -1;
}
return aDate - bDate;
},
hasMultipleDueDates (assignment) {
return uniqueEffectiveDueDates(assignment).length > 1;
},
getComparator (arrangeBy) {
if (arrangeBy === 'due_date') {
return this.compareByDueDate.bind(this);
}
if (arrangeBy === 'assignment_group') {
return this.compareByAssignmentGroup.bind(this);
}
},
compareByAssignmentGroup (a, b) {
const diffOfAssignmentGroupPosition = a.assignment_group_position - b.assignment_group_position;
if (diffOfAssignmentGroupPosition === 0) {
const diffOfAssignmentPosition = a.position - b.position;
if (diffOfAssignmentPosition === 0) { return 0 }
return diffOfAssignmentPosition;
}
return diffOfAssignmentGroupPosition;
}
};
}
export default assignmentHelper

View File

@ -18,94 +18,102 @@
import _ from 'underscore'
import I18n from 'i18n!gradebook'
var MessageStudentsWhoHelper = {
settings: function(assignment, students) {
return {
options: this.options(assignment),
title: assignment.name,
points_possible: assignment.points_possible,
students: students,
context_code: "course_" + assignment.course_id,
callback: this.callbackFn.bind(this),
subjectCallback: this.generateSubjectCallbackFn(assignment)
}
},
options: function(assignment) {
var options = this.allOptions();
var noSubmissions = !this.hasSubmission(assignment);
if(noSubmissions) options.splice(0,1);
return options;
},
allOptions: function() {
return [
{
text: I18n.t("students_who.havent_submitted_yet", "Haven't submitted yet"),
subjectFn: (assignment) => I18n.t('students_who.no_submission_for', 'No submission for %{assignment}', { assignment: assignment.name }),
criteriaFn: (student) => !student.submitted_at
},
{
text: I18n.t("students_who.havent_been_graded", "Haven't been graded"),
subjectFn: (assignment) => I18n.t('students_who.no_grade_for', 'No grade for %{assignment}', { assignment: assignment.name }),
criteriaFn: (student) => !this.exists(student.score)
},
{
text: I18n.t("students_who.scored_less_than", "Scored less than"),
cutoff: true,
subjectFn: (assignment, cutoff) => I18n.t(
'Scored less than %{cutoff} on %{assignment}',
{ assignment: assignment.name, cutoff: I18n.n(cutoff) }
),
criteriaFn: (student, cutoff) => this.scoreWithCutoff(student, cutoff) && student.score < cutoff
},
{
text: I18n.t("students_who.scored_more_than", "Scored more than"),
cutoff: true,
subjectFn: (assignment, cutoff) => I18n.t(
'Scored more than %{cutoff} on %{assignment}',
{ assignment: assignment.name, cutoff: I18n.n(cutoff) }
),
criteriaFn: (student, cutoff) => this.scoreWithCutoff(student, cutoff) && student.score > cutoff
}
];
},
hasSubmission: function(assignment) {
var submissionTypes = assignment.submission_types;
if(submissionTypes.length === 0) return false;
return _.any(submissionTypes, (submissionType) => {
return submissionType !== 'none' && submissionType !== 'on_paper';
});
},
exists: function(value) {
return !_.isUndefined(value) && !_.isNull(value);
},
scoreWithCutoff: function(student, cutoff) {
return this.exists(student.score)
&& student.score !== ''
&& this.exists(cutoff);
},
callbackFn: function(selected, cutoff, students) {
var criteriaFn = this.findOptionByText(selected).criteriaFn;
var students = _.filter(students, student => criteriaFn(student.user_data, cutoff));
return _.map(students, student => student.user_data.id);
},
findOptionByText: function(text) {
return _.find(this.allOptions(), option => option.text === text);
},
generateSubjectCallbackFn: function(assignment) {
return (selected, cutoff) => {
var cutoffString = cutoff || '';
var subjectFn = this.findOptionByText(selected).subjectFn;
return subjectFn(assignment, cutoffString);
}
var MessageStudentsWhoHelper = {
settings: function(assignment, students) {
return {
options: this.options(assignment),
title: assignment.name,
points_possible: assignment.points_possible,
students: students,
context_code: 'course_' + assignment.course_id,
callback: this.callbackFn.bind(this),
subjectCallback: this.generateSubjectCallbackFn(assignment)
}
};
},
options: function(assignment) {
var options = this.allOptions()
var noSubmissions = !this.hasSubmission(assignment)
if (noSubmissions) options.splice(0, 1)
return options
},
allOptions: function() {
return [
{
text: I18n.t('students_who.havent_submitted_yet', "Haven't submitted yet"),
subjectFn: assignment =>
I18n.t('students_who.no_submission_for', 'No submission for %{assignment}', {
assignment: assignment.name
}),
criteriaFn: student => !student.submitted_at
},
{
text: I18n.t('students_who.havent_been_graded', "Haven't been graded"),
subjectFn: assignment =>
I18n.t('students_who.no_grade_for', 'No grade for %{assignment}', {
assignment: assignment.name
}),
criteriaFn: student => !this.exists(student.score)
},
{
text: I18n.t('students_who.scored_less_than', 'Scored less than'),
cutoff: true,
subjectFn: (assignment, cutoff) =>
I18n.t('Scored less than %{cutoff} on %{assignment}', {
assignment: assignment.name,
cutoff: I18n.n(cutoff)
}),
criteriaFn: (student, cutoff) =>
this.scoreWithCutoff(student, cutoff) && student.score < cutoff
},
{
text: I18n.t('students_who.scored_more_than', 'Scored more than'),
cutoff: true,
subjectFn: (assignment, cutoff) =>
I18n.t('Scored more than %{cutoff} on %{assignment}', {
assignment: assignment.name,
cutoff: I18n.n(cutoff)
}),
criteriaFn: (student, cutoff) =>
this.scoreWithCutoff(student, cutoff) && student.score > cutoff
}
]
},
hasSubmission: function(assignment) {
var submissionTypes = assignment.submission_types
if (submissionTypes.length === 0) return false
return _.any(submissionTypes, submissionType => {
return submissionType !== 'none' && submissionType !== 'on_paper'
})
},
exists: function(value) {
return !_.isUndefined(value) && !_.isNull(value)
},
scoreWithCutoff: function(student, cutoff) {
return this.exists(student.score) && student.score !== '' && this.exists(cutoff)
},
callbackFn: function(selected, cutoff, students) {
var criteriaFn = this.findOptionByText(selected).criteriaFn
var students = _.filter(students, student => criteriaFn(student.user_data, cutoff))
return _.map(students, student => student.user_data.id)
},
findOptionByText: function(text) {
return _.find(this.allOptions(), option => option.text === text)
},
generateSubjectCallbackFn: function(assignment) {
return (selected, cutoff) => {
var cutoffString = cutoff || ''
var subjectFn = this.findOptionByText(selected).subjectFn
return subjectFn(assignment, cutoffString)
}
}
}
export default MessageStudentsWhoHelper

View File

@ -21,164 +21,188 @@ import $ from 'jquery'
import I18n from 'i18n!gradebook_upload'
import 'jquery.ajaxJSON'
const successMessage = I18n.t(
'You will be redirected to Gradebook while your file is being uploaded. ' +
'If you have a large CSV file, your changes may take a few minutes to update. ' +
'To prevent overwriting any data, please confirm the upload has completed and ' +
'Gradebook is correct before making additional changes.'
);
const successMessage = I18n.t(
'You will be redirected to Gradebook while your file is being uploaded. ' +
'If you have a large CSV file, your changes may take a few minutes to update. ' +
'To prevent overwriting any data, please confirm the upload has completed and ' +
'Gradebook is correct before making additional changes.'
)
const ProcessGradebookUpload = {
upload (gradebook) {
if (gradebook != null && (_.isArray(gradebook.assignments) || _.isArray(gradebook.custom_columns)) && _.isArray(gradebook.students)) {
if (gradebook.custom_columns && gradebook.custom_columns.length > 0) {
this.uploadCustomColumnData(gradebook);
}
const createAssignmentsResponses = this.createAssignments(gradebook);
return $.when(...createAssignmentsResponses).then((...responses) => {
this.uploadGradeData(gradebook, responses);
});
}
return undefined;
},
uploadCustomColumnData (gradebook) {
const customColumnData = gradebook.students.reduce((accumulator, student) => {
const student_id = Number.parseInt(student.id, 10);
if (!(student_id in accumulator)) {
accumulator[student_id] = student.custom_column_data // eslint-disable-line no-param-reassign
}
return accumulator;
}, {});
if (!_.isEmpty(customColumnData)) {
this.parseCustomColumnData(customColumnData);
const ProcessGradebookUpload = {
upload(gradebook) {
if (
gradebook != null &&
(_.isArray(gradebook.assignments) || _.isArray(gradebook.custom_columns)) &&
_.isArray(gradebook.students)
) {
if (gradebook.custom_columns && gradebook.custom_columns.length > 0) {
this.uploadCustomColumnData(gradebook)
}
if (!gradebook.assignments.length) {
alert(successMessage); // eslint-disable-line no-alert
this.goToGradebook();
}
},
parseCustomColumnData (customColumnData) {
const data = [];
Object.keys(customColumnData).forEach(studentId => {
customColumnData[studentId].forEach((column) => {
data.push({
column_id: Number.parseInt(column.column_id, 10),
user_id: studentId,
content: column.new_content
})
});
const createAssignmentsResponses = this.createAssignments(gradebook)
return $.when(...createAssignmentsResponses).then((...responses) => {
this.uploadGradeData(gradebook, responses)
})
}
return undefined
},
this.submitCustomColumnData(data);
return data;
},
uploadCustomColumnData(gradebook) {
const customColumnData = gradebook.students.reduce((accumulator, student) => {
const student_id = Number.parseInt(student.id, 10)
if (!(student_id in accumulator)) {
accumulator[student_id] = student.custom_column_data // eslint-disable-line no-param-reassign
}
return accumulator
}, {})
submitCustomColumnData (data) {
return $.ajaxJSON(ENV.bulk_update_custom_columns_path,
'PUT',
JSON.stringify({column_data: data}),
null,
null,
{contentType: 'application/json'});
},
if (!_.isEmpty(customColumnData)) {
this.parseCustomColumnData(customColumnData)
}
createAssignments (gradebook) {
const newAssignments = this.getNewAssignmentsFromGradebook(gradebook);
return newAssignments.map(assignment => this.createIndividualAssignment(assignment));
},
if (!gradebook.assignments.length) {
alert(successMessage) // eslint-disable-line no-alert
this.goToGradebook()
}
},
getNewAssignmentsFromGradebook (gradebook) {
return gradebook.assignments.filter(a => a.id != null && a.id <= 0);
},
parseCustomColumnData(customColumnData) {
const data = []
Object.keys(customColumnData).forEach(studentId => {
customColumnData[studentId].forEach(column => {
data.push({
column_id: Number.parseInt(column.column_id, 10),
user_id: studentId,
content: column.new_content
})
})
})
createIndividualAssignment (assignment) {
return $.ajaxJSON(ENV.create_assignment_path, 'POST', JSON.stringify({
this.submitCustomColumnData(data)
return data
},
submitCustomColumnData(data) {
return $.ajaxJSON(
ENV.bulk_update_custom_columns_path,
'PUT',
JSON.stringify({column_data: data}),
null,
null,
{contentType: 'application/json'}
)
},
createAssignments(gradebook) {
const newAssignments = this.getNewAssignmentsFromGradebook(gradebook)
return newAssignments.map(assignment => this.createIndividualAssignment(assignment))
},
getNewAssignmentsFromGradebook(gradebook) {
return gradebook.assignments.filter(a => a.id != null && a.id <= 0)
},
createIndividualAssignment(assignment) {
return $.ajaxJSON(
ENV.create_assignment_path,
'POST',
JSON.stringify({
assignment: {
name: assignment.title,
points_possible: assignment.points_possible,
published: true
},
calculate_grades: false
}), null, null, {contentType: 'application/json'});
},
}),
null,
null,
{contentType: 'application/json'}
)
},
uploadGradeData (gradebook, responses) {
const gradeData = this.populateGradeData(gradebook, responses);
uploadGradeData(gradebook, responses) {
const gradeData = this.populateGradeData(gradebook, responses)
if (_.isEmpty(gradeData)) {
this.goToGradebook();
} else {
this.submitGradeData(gradeData).then((progress) => {
alert(successMessage); // eslint-disable-line no-alert
ProcessGradebookUpload.goToGradebook();
});
}
},
populateGradeData (gradebook, responses) {
const assignmentMap = this.mapLocalAssignmentsToDatabaseAssignments(gradebook, responses);
const gradeData = {};
gradebook.students.forEach(student => this.populateGradeDataPerStudent(student, assignmentMap, gradeData));
return gradeData;
},
mapLocalAssignmentsToDatabaseAssignments (gradebook, responses) {
const newAssignments = this.getNewAssignmentsFromGradebook(gradebook);
let responsesLists = responses;
if (newAssignments.length === 1) {
responsesLists = [responses];
}
const assignmentMap = {};
_(newAssignments).zip(responsesLists).forEach((fakeAndCreated) => {
const [assignmentStub, response] = fakeAndCreated;
const [createdAssignment] = response;
assignmentMap[assignmentStub.id] = createdAssignment.id;
});
return assignmentMap;
},
populateGradeDataPerStudent (student, assignmentMap, gradeData) {
student.submissions.forEach((submission) => {
this.populateGradeDataPerSubmission(submission, student.previous_id, assignmentMap, gradeData);
});
},
populateGradeDataPerSubmission (submission, studentId, assignmentMap, gradeData) {
const assignmentId = assignmentMap[submission.assignment_id] || submission.assignment_id;
if (assignmentId <= 0) return; // unrecognized and ignored assignments
if (submission.original_grade === submission.grade) return; // no change
gradeData[assignmentId] = gradeData[assignmentId] || {}; // eslint-disable-line no-param-reassign
if (String(submission.grade || '').toUpperCase() === 'EX') {
gradeData[assignmentId][studentId] = { excuse: true }; // eslint-disable-line no-param-reassign
} else {
gradeData[assignmentId][studentId] = { // eslint-disable-line no-param-reassign
posted_grade: submission.grade
};
}
},
submitGradeData (gradeData) {
return $.ajaxJSON(ENV.bulk_update_path, 'POST', JSON.stringify({grade_data: gradeData}),
null, null, {contentType: 'application/json'});
},
goToGradebook () {
$('#gradebook_grid_form').text(I18n.t('Done.'));
window.location = ENV.gradebook_path;
if (_.isEmpty(gradeData)) {
this.goToGradebook()
} else {
this.submitGradeData(gradeData).then(progress => {
alert(successMessage) // eslint-disable-line no-alert
ProcessGradebookUpload.goToGradebook()
})
}
};
},
populateGradeData(gradebook, responses) {
const assignmentMap = this.mapLocalAssignmentsToDatabaseAssignments(gradebook, responses)
const gradeData = {}
gradebook.students.forEach(student =>
this.populateGradeDataPerStudent(student, assignmentMap, gradeData)
)
return gradeData
},
mapLocalAssignmentsToDatabaseAssignments(gradebook, responses) {
const newAssignments = this.getNewAssignmentsFromGradebook(gradebook)
let responsesLists = responses
if (newAssignments.length === 1) {
responsesLists = [responses]
}
const assignmentMap = {}
_(newAssignments)
.zip(responsesLists)
.forEach(fakeAndCreated => {
const [assignmentStub, response] = fakeAndCreated
const [createdAssignment] = response
assignmentMap[assignmentStub.id] = createdAssignment.id
})
return assignmentMap
},
populateGradeDataPerStudent(student, assignmentMap, gradeData) {
student.submissions.forEach(submission => {
this.populateGradeDataPerSubmission(submission, student.previous_id, assignmentMap, gradeData)
})
},
populateGradeDataPerSubmission(submission, studentId, assignmentMap, gradeData) {
const assignmentId = assignmentMap[submission.assignment_id] || submission.assignment_id
if (assignmentId <= 0) return // unrecognized and ignored assignments
if (submission.original_grade === submission.grade) return // no change
gradeData[assignmentId] = gradeData[assignmentId] || {} // eslint-disable-line no-param-reassign
if (String(submission.grade || '').toUpperCase() === 'EX') {
gradeData[assignmentId][studentId] = {excuse: true} // eslint-disable-line no-param-reassign
} else {
gradeData[assignmentId][studentId] = {
// eslint-disable-line no-param-reassign
posted_grade: submission.grade
}
}
},
submitGradeData(gradeData) {
return $.ajaxJSON(
ENV.bulk_update_path,
'POST',
JSON.stringify({grade_data: gradeData}),
null,
null,
{contentType: 'application/json'}
)
},
goToGradebook() {
$('#gradebook_grid_form').text(I18n.t('Done.'))
window.location = ENV.gradebook_path
}
}
export default ProcessGradebookUpload

View File

@ -238,23 +238,44 @@ test('does not disable "Set Default Grade" when isAdmin', function() {
ENV.current_user_roles = ['admin']
this.assignment = {inClosedGradingPeriod: true}
this.disableUnavailableMenuActions(this.menu)
strictEqual(this.menu.find('[data-action="setDefaultGrade"]')[0].getAttribute('aria-disabled'), null)
strictEqual(
this.menu.find('[data-action="setDefaultGrade"]')[0].getAttribute('aria-disabled'),
null
)
})
test('disables "Unmute Assignment" when the assignment is moderated and grades have not been published', function () {
this.assignment = {moderated_grading: true, grades_published: false, inClosedGradingPeriod: false, muted: true}
test('disables "Unmute Assignment" when the assignment is moderated and grades have not been published', function() {
this.assignment = {
moderated_grading: true,
grades_published: false,
inClosedGradingPeriod: false,
muted: true
}
this.disableUnavailableMenuActions(this.menu)
strictEqual(this.menu.find('[data-action="toggleMuting"]')[0].getAttribute('aria-disabled'), 'true')
strictEqual(
this.menu.find('[data-action="toggleMuting"]')[0].getAttribute('aria-disabled'),
'true'
)
})
test('does not disable "Unmute Assignment" when grades are published', function () {
this.assignment = {moderated_grading: true, grades_published: true, inClosedGradingPeriod: false, muted: true}
test('does not disable "Unmute Assignment" when grades are published', function() {
this.assignment = {
moderated_grading: true,
grades_published: true,
inClosedGradingPeriod: false,
muted: true
}
this.disableUnavailableMenuActions(this.menu)
strictEqual(this.menu.find('[data-action="toggleMuting"]')[0].getAttribute('aria-disabled'), null)
})
test('does not disable "Mute Assignment" when the assignment can be muted', function () {
this.assignment = {moderated_grading: true, grades_published: false, inClosedGradingPeriod: false, muted: false}
test('does not disable "Mute Assignment" when the assignment can be muted', function() {
this.assignment = {
moderated_grading: true,
grades_published: false,
inClosedGradingPeriod: false,
muted: false
}
this.disableUnavailableMenuActions(this.menu)
strictEqual(this.menu.find('[data-action="toggleMuting"]')[0].getAttribute('aria-disabled'), null)
})

View File

@ -366,7 +366,14 @@ QUnit.module('Gradebook#getVisibleGradeGridColumns', {
{object: {assignment_group: {position: 1}, position: 1, name: 'first'}},
{object: {assignment_group: {position: 1}, position: 2, name: 'second'}},
{object: {assignment_group: {position: 1}, position: 3, name: 'third'}},
{object: {assignment_group: {position: 1}, position: 4, name: 'moderated', moderation_in_progress: true}}
{
object: {
assignment_group: {position: 1},
position: 4,
name: 'moderated',
moderation_in_progress: true
}
}
]
this.aggregateColumns = []
this.parentColumns = []
@ -399,7 +406,10 @@ test('It does not sort columns when gradebookColumnOrderSettings is undefined',
})
test('sets cannot_edit if moderation_in_progress is true on the column object', function() {
const moderatedColumn = _.find(this.allAssignmentColumns, (column) => column.object.moderation_in_progress)
const moderatedColumn = _.find(
this.allAssignmentColumns,
column => column.object.moderation_in_progress
)
this.getVisibleGradeGridColumns()
strictEqual(moderatedColumn.cssClass, 'cannot_edit')
})
@ -414,16 +424,16 @@ QUnit.module('Gradebook#customColumnDefinitions', {
setup() {
this.gradebook = createGradebook()
this.gradebook.customColumns = [
{ id: '1', teacher_notes: false, hidden: false, title: 'Read Only', read_only: true },
{ id: '2', teacher_notes: false, hidden: false, title: 'Not Read Only', read_only: false }
{id: '1', teacher_notes: false, hidden: false, title: 'Read Only', read_only: true},
{id: '2', teacher_notes: false, hidden: false, title: 'Not Read Only', read_only: false}
]
}
})
test('includes the cannot_edit class for read_only columns', function () {
test('includes the cannot_edit class for read_only columns', function() {
columns = this.gradebook.customColumnDefinitions()
equal(columns[0].cssClass, "meta-cell custom_column cannot_edit")
equal(columns[1].cssClass, "meta-cell custom_column")
equal(columns[0].cssClass, 'meta-cell custom_column cannot_edit')
equal(columns[1].cssClass, 'meta-cell custom_column')
})
QUnit.module('Gradebook#fieldsToExcludeFromAssignments', {
@ -923,16 +933,18 @@ QUnit.module('Gradebook#gotAllAssignmentGroups', suiteHooks => {
anonymize_students: true
}
assignmentGroups = [{
id: 1,
assignments: [
unmoderatedAssignment,
moderatedUnpublishedAssignment,
moderatedPublishedAssignment,
anonymousUnmoderatedAssignment,
anonymousModeratedAssignment
]
}]
assignmentGroups = [
{
id: 1,
assignments: [
unmoderatedAssignment,
moderatedUnpublishedAssignment,
moderatedPublishedAssignment,
anonymousUnmoderatedAssignment,
anonymousModeratedAssignment
]
}
]
gradebook = createGradebook()
sinon.stub(gradebook, 'updateAssignmentEffectiveDueDates')
@ -948,7 +960,8 @@ QUnit.module('Gradebook#gotAllAssignmentGroups', suiteHooks => {
test('sets moderation_in_progress to true for a moderated assignment whose grades are not published', () => {
gradebook.gotAllAssignmentGroups(assignmentGroups)
strictEqual(moderatedUnpublishedAssignment.moderation_in_progress, true) })
strictEqual(moderatedUnpublishedAssignment.moderation_in_progress, true)
})
test('sets moderation_in_progress to false for a moderated assignment whose grades are published', () => {
gradebook.gotAllAssignmentGroups(assignmentGroups)
@ -961,7 +974,7 @@ QUnit.module('Gradebook#gotAllAssignmentGroups', suiteHooks => {
})
})
QUnit.module('Gradebook#calculateAndRoundGroupTotalScore', (hooks) => {
QUnit.module('Gradebook#calculateAndRoundGroupTotalScore', hooks => {
let gradebook
hooks.beforeEach(() => {
@ -980,13 +993,13 @@ QUnit.module('Gradebook#calculateAndRoundGroupTotalScore', (hooks) => {
test('avoids floating point calculation issues', () => {
const score = gradebook.calculateAndRoundGroupTotalScore(946.65, 1000)
const floatingPointResult = 946.65 / 1000 * 100
const floatingPointResult = (946.65 / 1000) * 100
strictEqual(floatingPointResult, 94.66499999999999)
strictEqual(score, 94.67)
})
})
QUnit.module('Gradebook#handleAssignmentMutingChange', (hooks) => {
QUnit.module('Gradebook#handleAssignmentMutingChange', hooks => {
let gradebook
let updatedAssignment
@ -998,7 +1011,7 @@ QUnit.module('Gradebook#handleAssignmentMutingChange', (hooks) => {
getColumns: sinon.stub().returns([{}]),
invalidateRow() {},
render() {},
setColumns() {},
setColumns() {}
}
gradebook.students = {4: {id: '4', name: 'fred'}}
gradebook.studentViewStudents = {6: {id: '6', name: 'fake fred'}}
@ -1008,7 +1021,10 @@ QUnit.module('Gradebook#handleAssignmentMutingChange', (hooks) => {
test('updates the anonymize_students attribute on the assignment', () => {
gradebook.handleAssignmentMutingChange(updatedAssignment)
strictEqual(gradebook.assignments[updatedAssignment.id].anonymize_students, updatedAssignment.anonymize_students)
strictEqual(
gradebook.assignments[updatedAssignment.id].anonymize_students,
updatedAssignment.anonymize_students
)
})
test('updates the muted attribute on the assignment', () => {

View File

@ -41,11 +41,11 @@ test('Grid.Util._toRow', () => {
Grid.students = {1: {}}
Grid.sections = {1: {}}
const rollup = {
links: { section: "1", user: "1" },
scores: [{ score: "3", hide_points: true, links: { outcome:"2" } }]
links: {section: '1', user: '1'},
scores: [{score: '3', hide_points: true, links: {outcome: '2'}}]
}
ok(
isEqual(Grid.Util._toRow([rollup], null).outcome_2, { score: "3", hide_points: true }),
isEqual(Grid.Util._toRow([rollup], null).outcome_2, {score: '3', hide_points: true}),
'correctly returns an object with a score and hide_points for a cell'
)
})
@ -55,17 +55,17 @@ test('Grid.Util.toRows', () => {
Grid.sections = {1: {}}
const rollups = [
{
links: { section: "1", user: "3" }
links: {section: '1', user: '3'}
},
{
links: { section: "1", user: "1" }
links: {section: '1', user: '1'}
},
{
links: { section: "1", user: "2" }
links: {section: '1', user: '2'}
}
]
ok(
isEqual(Grid.Util.toRows(rollups).map((r) => r.student.id), [3, 1, 2]),
isEqual(Grid.Util.toRows(rollups).map(r => r.student.id), [3, 1, 2]),
'returns rows in the same user order as rollups'
)
})
@ -74,14 +74,11 @@ test('Grid.View.masteryDetails', () => {
const outcome = {mastery_points: 5, points_possible: 10}
const spy = sinon.spy(Grid.View, 'legacyMasteryDetails')
Grid.View.masteryDetails(10, outcome)
ok(
spy.calledOnce,
'calls legacyMasteryDetails when no custom ratings defined'
)
ok(spy.calledOnce, 'calls legacyMasteryDetails when no custom ratings defined')
Grid.ratings = [
{points: 10, color: '00ff00', description: 'great'},
{points: 5, color: '0000ff', description: 'OK'},
{points: 0, color: 'ff0000', description: 'turrable'}
{points: 5, color: '0000ff', description: 'OK'},
{points: 0, color: 'ff0000', description: 'turrable'}
]
ok(
isEqual(Grid.View.masteryDetails(10, outcome), ['rating_0', '#00ff00', 'great']),
@ -109,8 +106,8 @@ test('Grid.View.masteryDetails with scaling', () => {
const outcome = {points_possible: 5}
Grid.ratings = [
{points: 10, color: '00ff00', description: 'great'},
{points: 5, color: '0000ff', description: 'OK'},
{points: 0, color: 'ff0000', description: 'turrable'}
{points: 5, color: '0000ff', description: 'OK'},
{points: 0, color: 'ff0000', description: 'turrable'}
]
ok(
isEqual(Grid.View.masteryDetails(5, outcome), ['rating_0', '#00ff00', 'great']),
@ -130,8 +127,8 @@ test('Grid.View.masteryDetails with scaling (points_possible 0)', () => {
const outcome = {mastery_points: 5, points_possible: 0}
Grid.ratings = [
{points: 10, color: '00ff00', description: 'great'},
{points: 5, color: '0000ff', description: 'OK'},
{points: 0, color: 'ff0000', description: 'turrable'}
{points: 5, color: '0000ff', description: 'OK'},
{points: 0, color: 'ff0000', description: 'turrable'}
]
ok(
isEqual(Grid.View.masteryDetails(5, outcome), ['rating_0', '#00ff00', 'great']),
@ -166,7 +163,11 @@ test('Grid.View.legacyMasteryDetails', () => {
'returns "near-mastery" if half of mastery score or greater'
)
ok(
isEqual(Grid.View.legacyMasteryDetails(1, outcome), ['rating_3', '#EE0612', 'Well Below Mastery']),
isEqual(Grid.View.legacyMasteryDetails(1, outcome), [
'rating_3',
'#EE0612',
'Well Below Mastery'
]),
'returns "remedial" if less than half of mastery score'
)
})
@ -188,16 +189,15 @@ test('Grid.Util._studentColumn does not modify default options', () => {
test('Grid.Util.toColumns hasResults', () => {
const outcomes = [
{
id: "1"
id: '1'
},
{
id: "2"
id: '2'
}
]
const rollup = {
links: { section: "1", user: "1" },
scores: [{ score: "3", hide_points: true, links: { outcome:"2" } }]
links: {section: '1', user: '1'},
scores: [{score: '3', hide_points: true, links: {outcome: '2'}}]
}
const columns = Grid.Util.toColumns(outcomes, [rollup])
ok(isEqual(columns[1].hasResults, false))

View File

@ -80,7 +80,8 @@ test('#loadValue escapes html', function() {
})
test('#class.formatter rounds numbers if they are numbers', function() {
sandbox.stub(SubmissionCell.prototype, 'cellWrapper')
sandbox
.stub(SubmissionCell.prototype, 'cellWrapper')
.withArgs('0.67')
.returns('ok')
const formattedResponse = SubmissionCell.formatter(0, 0, {grade: 0.666}, {}, {})
@ -88,7 +89,8 @@ test('#class.formatter rounds numbers if they are numbers', function() {
})
test('#class.formatter gives the value to the formatter if submission.grade isnt a parseable number', function() {
sandbox.stub(SubmissionCell.prototype, 'cellWrapper')
sandbox
.stub(SubmissionCell.prototype, 'cellWrapper')
.withArgs('happy')
.returns('ok')
const formattedResponse = SubmissionCell.formatter(0, 0, {grade: 'happy'}, {}, {})
@ -96,7 +98,8 @@ test('#class.formatter gives the value to the formatter if submission.grade isnt
})
test('#class.formatter adds a percent symbol for assignments with a percent grading_type', function() {
sandbox.stub(SubmissionCell.prototype, 'cellWrapper')
sandbox
.stub(SubmissionCell.prototype, 'cellWrapper')
.withArgs('73%')
.returns('ok')
const formattedResponse = SubmissionCell.formatter(
@ -218,7 +221,8 @@ test("#class.formatter, isConcluded doesn't have grayed-out", () => {
})
test('#letter_grade.formatter, shows EX when submission is excused', function() {
sandbox.stub(SubmissionCell.prototype, 'cellWrapper')
sandbox
.stub(SubmissionCell.prototype, 'cellWrapper')
.withArgs('EX')
.returns('ok')
const formattedResponse = SubmissionCell.letter_grade.formatter(0, 0, {excused: true}, {}, {})
@ -226,7 +230,8 @@ test('#letter_grade.formatter, shows EX when submission is excused', function()
})
test('#letter_grade.formatter, shows the score and letter grade', function() {
sandbox.stub(SubmissionCell.prototype, 'cellWrapper')
sandbox
.stub(SubmissionCell.prototype, 'cellWrapper')
.withArgs("F<span class='letter-grade-points'>0</span>")
.returns('ok')
const formattedResponse = SubmissionCell.letter_grade.formatter(
@ -243,7 +248,8 @@ test('#letter_grade.formatter, shows the score and letter grade', function() {
})
test('#letter_grade.formatter, shows the letter grade', function() {
sandbox.stub(SubmissionCell.prototype, 'cellWrapper')
sandbox
.stub(SubmissionCell.prototype, 'cellWrapper')
.withArgs('B')
.returns('ok')
const formattedResponse = SubmissionCell.letter_grade.formatter(0, 0, {grade: 'B'}, {}, {})
@ -574,7 +580,10 @@ QUnit.module('SubmissionCell#classesBasedOnSubmission', () => {
test('does not return moderated if anonymize_students is set on the assignment', () => {
const assignment = {anonymize_students: true}
strictEqual(SubmissionCell.classesBasedOnSubmission({}, assignment).includes('moderated'), false)
strictEqual(
SubmissionCell.classesBasedOnSubmission({}, assignment).includes('moderated'),
false
)
})
test('returns muted when muted is set on the assignment', () => {

View File

@ -88,12 +88,16 @@ test('speed_grader_enabled as false does not set speedgrader url', function() {
test('speedgrader url quotes the student id', function() {
// Supply a value for context_url so we have a well-formed speedGraderUrl
this.options.context_url = 'http://localhost';
const submissionDetailsDialog = new SubmissionDetailsDialog(this.assignment, this.user, this.options);
this.options.context_url = 'http://localhost'
const submissionDetailsDialog = new SubmissionDetailsDialog(
this.assignment,
this.user,
this.options
)
const urlObject = new URL(submissionDetailsDialog.submission.speedGraderUrl);
strictEqual(decodeURI(urlObject.hash), '#{"student_id":"1"}');
submissionDetailsDialog.dialog.dialog('destroy');
const urlObject = new URL(submissionDetailsDialog.submission.speedGraderUrl)
strictEqual(decodeURI(urlObject.hash), '#{"student_id":"1"}')
submissionDetailsDialog.dialog.dialog('destroy')
})
test('lateness correctly passes through to the template', function() {

File diff suppressed because it is too large Load Diff

View File

@ -16,12 +16,12 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import _ from 'underscore';
import timezone from 'timezone';
import { scopeToUser, updateWithSubmissions } from 'jsx/gradebook/EffectiveDueDates';
import _ from 'underscore'
import timezone from 'timezone'
import {scopeToUser, updateWithSubmissions} from 'jsx/gradebook/EffectiveDueDates'
QUnit.module('EffectiveDueDates', function () {
QUnit.module('.scopeToUser', function () {
QUnit.module('EffectiveDueDates', function() {
QUnit.module('.scopeToUser', function() {
const exampleDueDatesData = {
201: {
101: {
@ -42,38 +42,42 @@ QUnit.module('EffectiveDueDates', function () {
in_closed_grading_period: false
}
}
};
}
test('returns a map with effective due dates keyed to assignment ids', () => {
const scopedDueDates = scopeToUser(exampleDueDatesData, '101');
deepEqual(_.keys(scopedDueDates).sort(), ['201', '202']);
deepEqual(_.keys(scopedDueDates[201]).sort(), ['due_at', 'grading_period_id', 'in_closed_grading_period']);
});
const scopedDueDates = scopeToUser(exampleDueDatesData, '101')
deepEqual(_.keys(scopedDueDates).sort(), ['201', '202'])
deepEqual(_.keys(scopedDueDates[201]).sort(), [
'due_at',
'grading_period_id',
'in_closed_grading_period'
])
})
test('includes all effective due dates for the given user', () => {
const scopedDueDates = scopeToUser(exampleDueDatesData, '101');
equal(scopedDueDates[201].due_at, '2015-05-04T12:00:00Z');
equal(scopedDueDates[201].grading_period_id, '701');
equal(scopedDueDates[201].in_closed_grading_period, true);
equal(scopedDueDates[202].due_at, '2015-06-04T12:00:00Z');
equal(scopedDueDates[202].grading_period_id, '702');
equal(scopedDueDates[202].in_closed_grading_period, false);
});
const scopedDueDates = scopeToUser(exampleDueDatesData, '101')
equal(scopedDueDates[201].due_at, '2015-05-04T12:00:00Z')
equal(scopedDueDates[201].grading_period_id, '701')
equal(scopedDueDates[201].in_closed_grading_period, true)
equal(scopedDueDates[202].due_at, '2015-06-04T12:00:00Z')
equal(scopedDueDates[202].grading_period_id, '702')
equal(scopedDueDates[202].in_closed_grading_period, false)
})
test('excludes assignments not assigned to the given user', () => {
const scopedDueDates = scopeToUser(exampleDueDatesData, '102');
deepEqual(_.keys(scopedDueDates), ['201']);
equal(scopedDueDates[201].due_at, '2015-05-05T12:00:00Z');
equal(scopedDueDates[201].grading_period_id, '701');
equal(scopedDueDates[201].in_closed_grading_period, true);
});
});
const scopedDueDates = scopeToUser(exampleDueDatesData, '102')
deepEqual(_.keys(scopedDueDates), ['201'])
equal(scopedDueDates[201].due_at, '2015-05-05T12:00:00Z')
equal(scopedDueDates[201].grading_period_id, '701')
equal(scopedDueDates[201].in_closed_grading_period, true)
})
})
QUnit.module('.updateWithSubmissions', function (hooks) {
let effectiveDueDates;
let submissions;
QUnit.module('.updateWithSubmissions', function(hooks) {
let effectiveDueDates
let submissions
hooks.beforeEach(function () {
hooks.beforeEach(function() {
effectiveDueDates = {
2301: {
1101: {
@ -94,13 +98,13 @@ QUnit.module('EffectiveDueDates', function () {
in_closed_grading_period: false
}
}
};
}
submissions = [
{ assignment_id: '2301', user_id: '1101', cached_due_date: '2015-02-01T12:00:00Z' },
{ assignment_id: '2302', user_id: '1101', cached_due_date: '2015-04-01T12:00:00Z' },
{ assignment_id: '2302', user_id: '1102', cached_due_date: '2015-04-02T12:00:00Z' }
];
});
{assignment_id: '2301', user_id: '1101', cached_due_date: '2015-02-01T12:00:00Z'},
{assignment_id: '2302', user_id: '1101', cached_due_date: '2015-04-01T12:00:00Z'},
{assignment_id: '2302', user_id: '1102', cached_due_date: '2015-04-02T12:00:00Z'}
]
})
const gradingPeriods = [
{
@ -124,77 +128,77 @@ QUnit.module('EffectiveDueDates', function () {
isClosed: false,
startDate: timezone.parse('2015-03-01T12:00:00Z')
}
];
]
test('sets the due_at for each effective due date', function () {
effectiveDueDates = {};
updateWithSubmissions(effectiveDueDates, submissions, gradingPeriods);
equal(effectiveDueDates[2301][1101].due_at, '2015-02-01T12:00:00Z');
equal(effectiveDueDates[2302][1101].due_at, '2015-04-01T12:00:00Z');
equal(effectiveDueDates[2302][1102].due_at, '2015-04-02T12:00:00Z');
});
test('sets the due_at for each effective due date', function() {
effectiveDueDates = {}
updateWithSubmissions(effectiveDueDates, submissions, gradingPeriods)
equal(effectiveDueDates[2301][1101].due_at, '2015-02-01T12:00:00Z')
equal(effectiveDueDates[2302][1101].due_at, '2015-04-01T12:00:00Z')
equal(effectiveDueDates[2302][1102].due_at, '2015-04-02T12:00:00Z')
})
test('sets the grading_period_id for each effective due date', function () {
effectiveDueDates = {};
updateWithSubmissions(effectiveDueDates, submissions, gradingPeriods);
strictEqual(effectiveDueDates[2301][1101].grading_period_id, '1401');
strictEqual(effectiveDueDates[2302][1101].grading_period_id, '1402');
strictEqual(effectiveDueDates[2302][1102].grading_period_id, '1402');
});
test('sets the grading_period_id for each effective due date', function() {
effectiveDueDates = {}
updateWithSubmissions(effectiveDueDates, submissions, gradingPeriods)
strictEqual(effectiveDueDates[2301][1101].grading_period_id, '1401')
strictEqual(effectiveDueDates[2302][1101].grading_period_id, '1402')
strictEqual(effectiveDueDates[2302][1102].grading_period_id, '1402')
})
test('sets in_closed_grading_period for each effective due date', function () {
effectiveDueDates = {};
updateWithSubmissions(effectiveDueDates, submissions, gradingPeriods);
strictEqual(effectiveDueDates[2301][1101].in_closed_grading_period, true);
strictEqual(effectiveDueDates[2302][1101].in_closed_grading_period, false);
strictEqual(effectiveDueDates[2302][1102].in_closed_grading_period, false);
});
test('sets in_closed_grading_period for each effective due date', function() {
effectiveDueDates = {}
updateWithSubmissions(effectiveDueDates, submissions, gradingPeriods)
strictEqual(effectiveDueDates[2301][1101].in_closed_grading_period, true)
strictEqual(effectiveDueDates[2302][1101].in_closed_grading_period, false)
strictEqual(effectiveDueDates[2302][1102].in_closed_grading_period, false)
})
test('updates existing effective due dates for students', function () {
updateWithSubmissions(effectiveDueDates, submissions, gradingPeriods);
equal(effectiveDueDates[2301][1101].due_at, '2015-02-01T12:00:00Z');
strictEqual(effectiveDueDates[2301][1101].grading_period_id, '1401');
strictEqual(effectiveDueDates[2301][1101].in_closed_grading_period, true);
});
test('updates existing effective due dates for students', function() {
updateWithSubmissions(effectiveDueDates, submissions, gradingPeriods)
equal(effectiveDueDates[2301][1101].due_at, '2015-02-01T12:00:00Z')
strictEqual(effectiveDueDates[2301][1101].grading_period_id, '1401')
strictEqual(effectiveDueDates[2301][1101].in_closed_grading_period, true)
})
test('preserves effective due dates for unrelated students', function () {
updateWithSubmissions(effectiveDueDates, submissions, gradingPeriods);
equal(effectiveDueDates[2301][1103].due_at, '2015-02-02T12:00:00Z');
strictEqual(effectiveDueDates[2301][1103].grading_period_id, '1401');
strictEqual(effectiveDueDates[2301][1103].in_closed_grading_period, true);
});
test('preserves effective due dates for unrelated students', function() {
updateWithSubmissions(effectiveDueDates, submissions, gradingPeriods)
equal(effectiveDueDates[2301][1103].due_at, '2015-02-02T12:00:00Z')
strictEqual(effectiveDueDates[2301][1103].grading_period_id, '1401')
strictEqual(effectiveDueDates[2301][1103].in_closed_grading_period, true)
})
test('preserves effective due dates for unrelated assignments', function () {
updateWithSubmissions(effectiveDueDates, submissions, gradingPeriods);
equal(effectiveDueDates[2303][1101].due_at, '2015-04-02T12:00:00Z');
strictEqual(effectiveDueDates[2303][1101].grading_period_id, '1402');
strictEqual(effectiveDueDates[2303][1101].in_closed_grading_period, false);
});
test('preserves effective due dates for unrelated assignments', function() {
updateWithSubmissions(effectiveDueDates, submissions, gradingPeriods)
equal(effectiveDueDates[2303][1101].due_at, '2015-04-02T12:00:00Z')
strictEqual(effectiveDueDates[2303][1101].grading_period_id, '1402')
strictEqual(effectiveDueDates[2303][1101].in_closed_grading_period, false)
})
test('uses the last grading period when the cached due date is null', function () {
effectiveDueDates = {};
submissions[0].cached_due_date = null;
updateWithSubmissions(effectiveDueDates, submissions, gradingPeriods);
strictEqual(effectiveDueDates[2301][1101].due_at, null);
strictEqual(effectiveDueDates[2301][1101].grading_period_id, '1403');
strictEqual(effectiveDueDates[2301][1101].in_closed_grading_period, false);
});
test('uses the last grading period when the cached due date is null', function() {
effectiveDueDates = {}
submissions[0].cached_due_date = null
updateWithSubmissions(effectiveDueDates, submissions, gradingPeriods)
strictEqual(effectiveDueDates[2301][1101].due_at, null)
strictEqual(effectiveDueDates[2301][1101].grading_period_id, '1403')
strictEqual(effectiveDueDates[2301][1101].in_closed_grading_period, false)
})
test('uses no grading period when the cached due date is outside any grading period', function () {
effectiveDueDates = {};
submissions[0].cached_due_date = '2015-07-02T12:00:00Z';
updateWithSubmissions(effectiveDueDates, submissions, gradingPeriods);
strictEqual(effectiveDueDates[2301][1101].due_at, '2015-07-02T12:00:00Z');
strictEqual(effectiveDueDates[2301][1101].grading_period_id, null);
strictEqual(effectiveDueDates[2301][1101].in_closed_grading_period, false);
});
test('uses no grading period when the cached due date is outside any grading period', function() {
effectiveDueDates = {}
submissions[0].cached_due_date = '2015-07-02T12:00:00Z'
updateWithSubmissions(effectiveDueDates, submissions, gradingPeriods)
strictEqual(effectiveDueDates[2301][1101].due_at, '2015-07-02T12:00:00Z')
strictEqual(effectiveDueDates[2301][1101].grading_period_id, null)
strictEqual(effectiveDueDates[2301][1101].in_closed_grading_period, false)
})
test('uses no grading period when not given any grading periods', function () {
effectiveDueDates = {};
updateWithSubmissions(effectiveDueDates, submissions, undefined);
strictEqual(effectiveDueDates[2301][1101].due_at, '2015-02-01T12:00:00Z');
strictEqual(effectiveDueDates[2301][1101].grading_period_id, null);
strictEqual(effectiveDueDates[2301][1101].in_closed_grading_period, false);
});
});
});
test('uses no grading period when not given any grading periods', function() {
effectiveDueDates = {}
updateWithSubmissions(effectiveDueDates, submissions, undefined)
strictEqual(effectiveDueDates[2301][1101].due_at, '2015-02-01T12:00:00Z')
strictEqual(effectiveDueDates[2301][1101].grading_period_id, null)
strictEqual(effectiveDueDates[2301][1101].in_closed_grading_period, false)
})
})
})

View File

@ -16,7 +16,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import fakeENV from 'helpers/fakeENV'
import fakeENV from 'helpers/fakeENV'
import PostGradesFrameDialog from 'compiled/gradebook/PostGradesFrameDialog'
let dialog
@ -25,7 +25,7 @@ let iframe
let info
QUnit.module('PostGradesFrameDialog screenreader only content', {
setup () {
setup() {
fakeENV.setup({LTI_LAUNCH_FRAME_ALLOWANCES: ['midi', 'media']})
dialog = new PostGradesFrameDialog({})
dialog.open()
@ -33,7 +33,7 @@ QUnit.module('PostGradesFrameDialog screenreader only content', {
iframe = el.find('iframe')
},
teardown () {
teardown() {
dialog.close()
dialog.$dialog.remove()
fakeENV.teardown()
@ -71,7 +71,12 @@ test('hides ending info alert and removes class from iframe', () => {
})
test("doesn't show infos or add border to iframe by default", () => {
equal(el.find('.before_external_content_info_alert.screenreader-only, .after_external_content_info_alert.screenreader-only').length, 2)
equal(
el.find(
'.before_external_content_info_alert.screenreader-only, .after_external_content_info_alert.screenreader-only'
).length,
2
)
notOk(iframe.hasClass('info_alert_outline'))
})

View File

@ -16,24 +16,24 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import Gradebook from 'compiled/gradebook/Gradebook';
import _ from 'underscore';
import fakeENV from 'helpers/fakeENV';
import UserSettings from 'compiled/userSettings';
import $ from 'jquery';
import Gradebook from 'compiled/gradebook/Gradebook'
import _ from 'underscore'
import fakeENV from 'helpers/fakeENV'
import UserSettings from 'compiled/userSettings'
import $ from 'jquery'
function createGradebook (opts = {}) {
return new Gradebook({ settings: {}, sections: {}, ...opts });
function createGradebook(opts = {}) {
return new Gradebook({settings: {}, sections: {}, ...opts})
}
QUnit.module('addRow', {
setup () {
setup() {
fakeENV.setup({
GRADEBOOK_OPTIONS: { context_id: 1 },
});
GRADEBOOK_OPTIONS: {context_id: 1}
})
},
teardown: () => fakeENV.teardown(),
});
teardown: () => fakeENV.teardown()
})
test("doesn't add filtered out users", () => {
const gb = {
@ -43,81 +43,81 @@ test("doesn't add filtered out users", () => {
rows: [],
sectionToShow: '2', // this is the filter
...Gradebook.prototype
};
}
const student1 = {
enrollments: [{grades: {}}],
sections: ['1'],
name: 'student',
};
const student2 = {...student1, sections: ['2']};
const student3 = {...student1, sections: ['2']};
[student1, student2, student3].forEach(s => gb.addRow(s));
name: 'student'
}
const student2 = {...student1, sections: ['2']}
const student3 = {...student1, sections: ['2']}
;[student1, student2, student3].forEach(s => gb.addRow(s))
ok(student1.row == null, 'filtered out students get no row number');
ok(student2.row === 0, 'other students do get a row number');
ok(student3.row === 1, 'row number increments');
ok(_.isEqual(gb.rows, [student2, student3]));
});
ok(student1.row == null, 'filtered out students get no row number')
ok(student2.row === 0, 'other students do get a row number')
ok(student3.row === 1, 'row number increments')
ok(_.isEqual(gb.rows, [student2, student3]))
})
QUnit.module('Gradebook#groupTotalFormatter', {
setup () {
fakeENV.setup();
setup() {
fakeENV.setup()
},
teardown () {
fakeENV.teardown();
},
});
teardown() {
fakeENV.teardown()
}
})
test('calculates percentage from given score and possible values', function () {
const gradebook = new Gradebook({ settings: {}, sections: {} });
const groupTotalOutput = gradebook.groupTotalFormatter(0, 0, { score: 9, possible: 10 }, {});
ok(groupTotalOutput.includes('9 / 10'));
ok(groupTotalOutput.includes('90%'));
});
test('calculates percentage from given score and possible values', function() {
const gradebook = new Gradebook({settings: {}, sections: {}})
const groupTotalOutput = gradebook.groupTotalFormatter(0, 0, {score: 9, possible: 10}, {})
ok(groupTotalOutput.includes('9 / 10'))
ok(groupTotalOutput.includes('90%'))
})
test('displays percentage as "-" when group total score is positive infinity', function () {
const gradebook = new Gradebook({ settings: {}, sections: {} });
sandbox.stub(gradebook, 'calculateAndRoundGroupTotalScore').returns(Number.POSITIVE_INFINITY);
const groupTotalOutput = gradebook.groupTotalFormatter(0, 0, { score: 9, possible: 0 }, {});
ok(groupTotalOutput.includes('9 / 0'));
ok(groupTotalOutput.includes('-'));
});
test('displays percentage as "-" when group total score is positive infinity', function() {
const gradebook = new Gradebook({settings: {}, sections: {}})
sandbox.stub(gradebook, 'calculateAndRoundGroupTotalScore').returns(Number.POSITIVE_INFINITY)
const groupTotalOutput = gradebook.groupTotalFormatter(0, 0, {score: 9, possible: 0}, {})
ok(groupTotalOutput.includes('9 / 0'))
ok(groupTotalOutput.includes('-'))
})
test('displays percentage as "-" when group total score is negative infinity', function () {
const gradebook = new Gradebook({ settings: {}, sections: {} });
sandbox.stub(gradebook, 'calculateAndRoundGroupTotalScore').returns(Number.NEGATIVE_INFINITY);
const groupTotalOutput = gradebook.groupTotalFormatter(0, 0, { score: 9, possible: 0 }, {});
ok(groupTotalOutput.includes('9 / 0'));
ok(groupTotalOutput.includes('-'));
});
test('displays percentage as "-" when group total score is negative infinity', function() {
const gradebook = new Gradebook({settings: {}, sections: {}})
sandbox.stub(gradebook, 'calculateAndRoundGroupTotalScore').returns(Number.NEGATIVE_INFINITY)
const groupTotalOutput = gradebook.groupTotalFormatter(0, 0, {score: 9, possible: 0}, {})
ok(groupTotalOutput.includes('9 / 0'))
ok(groupTotalOutput.includes('-'))
})
test('displays percentage as "-" when group total score is not a number', function () {
const gradebook = new Gradebook({ settings: {}, sections: {} });
sandbox.stub(gradebook, 'calculateAndRoundGroupTotalScore').returns(NaN);
const groupTotalOutput = gradebook.groupTotalFormatter(0, 0, { score: 9, possible: 0 }, {});
ok(groupTotalOutput.includes('9 / 0'));
ok(groupTotalOutput.includes('-'));
});
test('displays percentage as "-" when group total score is not a number', function() {
const gradebook = new Gradebook({settings: {}, sections: {}})
sandbox.stub(gradebook, 'calculateAndRoundGroupTotalScore').returns(NaN)
const groupTotalOutput = gradebook.groupTotalFormatter(0, 0, {score: 9, possible: 0}, {})
ok(groupTotalOutput.includes('9 / 0'))
ok(groupTotalOutput.includes('-'))
})
QUnit.module('Gradebook#getFrozenColumnCount');
QUnit.module('Gradebook#getFrozenColumnCount')
test('returns number of columns in frozen section', function () {
const gradebook = new Gradebook({ settings: {}, sections: {} });
gradebook.parentColumns = [{ id: 'student' }, { id: 'secondary_identifier' }];
gradebook.customColumns = [{ id: 'custom_col_1' }];
equal(gradebook.getFrozenColumnCount(), 3);
});
test('returns number of columns in frozen section', function() {
const gradebook = new Gradebook({settings: {}, sections: {}})
gradebook.parentColumns = [{id: 'student'}, {id: 'secondary_identifier'}]
gradebook.customColumns = [{id: 'custom_col_1'}]
equal(gradebook.getFrozenColumnCount(), 3)
})
QUnit.module('Gradebook#switchTotalDisplay', {
setupThis ({ showTotalGradeAsPoints = true } = {}) {
setupThis({showTotalGradeAsPoints = true} = {}) {
return {
options: {
show_total_grade_as_points: showTotalGradeAsPoints,
setting_update_url: 'http://settingUpdateUrl'
},
displayPointTotals () {
return true;
displayPointTotals() {
return true
},
grid: {
invalidate: sinon.stub()
@ -128,65 +128,65 @@ QUnit.module('Gradebook#switchTotalDisplay', {
}
},
setup () {
sandbox.stub($, 'ajaxJSON');
this.switchTotalDisplay = Gradebook.prototype.switchTotalDisplay;
setup() {
sandbox.stub($, 'ajaxJSON')
this.switchTotalDisplay = Gradebook.prototype.switchTotalDisplay
},
teardown () {
UserSettings.contextRemove('warned_about_totals_display');
teardown() {
UserSettings.contextRemove('warned_about_totals_display')
}
});
})
test('sets the warned_about_totals_display setting when called with true', function () {
notOk(UserSettings.contextGet('warned_about_totals_display'));
test('sets the warned_about_totals_display setting when called with true', function() {
notOk(UserSettings.contextGet('warned_about_totals_display'))
const self = this.setupThis();
this.switchTotalDisplay.call(self, { dontWarnAgain: true });
const self = this.setupThis()
this.switchTotalDisplay.call(self, {dontWarnAgain: true})
ok(UserSettings.contextGet('warned_about_totals_display'));
});
ok(UserSettings.contextGet('warned_about_totals_display'))
})
test('flips the show_total_grade_as_points property', function () {
const self = this.setupThis();
this.switchTotalDisplay.call(self, { dontWarnAgain: false });
test('flips the show_total_grade_as_points property', function() {
const self = this.setupThis()
this.switchTotalDisplay.call(self, {dontWarnAgain: false})
equal(self.options.show_total_grade_as_points, false);
equal(self.options.show_total_grade_as_points, false)
this.switchTotalDisplay.call(self, { dontWarnAgain: false });
this.switchTotalDisplay.call(self, {dontWarnAgain: false})
equal(self.options.show_total_grade_as_points, true);
});
equal(self.options.show_total_grade_as_points, true)
})
test('updates the total display preferences for the current user', function () {
const self = this.setupThis({ showTotalGradeAsPoints: false });
this.switchTotalDisplay.call(self, { dontWarnAgain: false });
test('updates the total display preferences for the current user', function() {
const self = this.setupThis({showTotalGradeAsPoints: false})
this.switchTotalDisplay.call(self, {dontWarnAgain: false})
equal($.ajaxJSON.callCount, 1);
equal($.ajaxJSON.getCall(0).args[0], 'http://settingUpdateUrl');
equal($.ajaxJSON.getCall(0).args[1], 'PUT');
equal($.ajaxJSON.getCall(0).args[2].show_total_grade_as_points, true);
});
equal($.ajaxJSON.callCount, 1)
equal($.ajaxJSON.getCall(0).args[0], 'http://settingUpdateUrl')
equal($.ajaxJSON.getCall(0).args[1], 'PUT')
equal($.ajaxJSON.getCall(0).args[2].show_total_grade_as_points, true)
})
test('invalidates the grid so it re-renders it', function () {
const self = this.setupThis();
this.switchTotalDisplay.call(self, { dontWarnAgain: false });
test('invalidates the grid so it re-renders it', function() {
const self = this.setupThis()
this.switchTotalDisplay.call(self, {dontWarnAgain: false})
equal(self.grid.invalidate.callCount, 1);
});
equal(self.grid.invalidate.callCount, 1)
})
test('updates the total grade column header with the new value of the show_total_grade_as_points property', function () {
const self = this.setupThis();
this.switchTotalDisplay.call(self, false);
test('updates the total grade column header with the new value of the show_total_grade_as_points property', function() {
const self = this.setupThis()
this.switchTotalDisplay.call(self, false)
this.switchTotalDisplay.call(self, false)
equal(self.totalHeader.switchTotalDisplay.callCount, 2);
equal(self.totalHeader.switchTotalDisplay.getCall(0).args[0], false);
equal(self.totalHeader.switchTotalDisplay.getCall(1).args[0], true);
});
equal(self.totalHeader.switchTotalDisplay.callCount, 2)
equal(self.totalHeader.switchTotalDisplay.getCall(0).args[0], false)
equal(self.totalHeader.switchTotalDisplay.getCall(1).args[0], true)
})
QUnit.module('Gradebook#togglePointsOrPercentTotals', {
setupThis () {
setupThis() {
return {
options: {
show_total_grade_as_points: true,
@ -196,95 +196,106 @@ QUnit.module('Gradebook#togglePointsOrPercentTotals', {
}
},
setup () {
sandbox.stub($, 'ajaxJSON');
this.togglePointsOrPercentTotals = Gradebook.prototype.togglePointsOrPercentTotals;
setup() {
sandbox.stub($, 'ajaxJSON')
this.togglePointsOrPercentTotals = Gradebook.prototype.togglePointsOrPercentTotals
},
teardown () {
UserSettings.contextRemove('warned_about_totals_display');
$(".ui-dialog").remove();
teardown() {
UserSettings.contextRemove('warned_about_totals_display')
$('.ui-dialog').remove()
}
});
})
test('when user is ignoring warnings, immediately toggles the total grade display', function () {
UserSettings.contextSet('warned_about_totals_display', true);
test('when user is ignoring warnings, immediately toggles the total grade display', function() {
UserSettings.contextSet('warned_about_totals_display', true)
const self = this.setupThis(true);
const self = this.setupThis(true)
this.togglePointsOrPercentTotals.call(self);
this.togglePointsOrPercentTotals.call(self)
equal(self.switchTotalDisplay.callCount, 1, 'toggles the total grade display');
});
equal(self.switchTotalDisplay.callCount, 1, 'toggles the total grade display')
})
test('when user is not ignoring warnings, return a dialog', function () {
UserSettings.contextSet('warned_about_totals_display', false);
test('when user is not ignoring warnings, return a dialog', function() {
UserSettings.contextSet('warned_about_totals_display', false)
const self = this.setupThis(true);
const dialog = this.togglePointsOrPercentTotals.call(self);
const self = this.setupThis(true)
const dialog = this.togglePointsOrPercentTotals.call(self)
equal(dialog.constructor.name, 'GradeDisplayWarningDialog', 'returns a grade display warning dialog');
equal(
dialog.constructor.name,
'GradeDisplayWarningDialog',
'returns a grade display warning dialog'
)
dialog.cancel();
});
dialog.cancel()
})
test('when user is not ignoring warnings, the dialog has a save property which is the switchTotalDisplay function', function () {
sandbox.stub(UserSettings, 'contextGet').withArgs('warned_about_totals_display').returns(false);
const self = this.setupThis(true);
const dialog = this.togglePointsOrPercentTotals.call(self);
test('when user is not ignoring warnings, the dialog has a save property which is the switchTotalDisplay function', function() {
sandbox
.stub(UserSettings, 'contextGet')
.withArgs('warned_about_totals_display')
.returns(false)
const self = this.setupThis(true)
const dialog = this.togglePointsOrPercentTotals.call(self)
equal(dialog.options.save, self.switchTotalDisplay);
equal(dialog.options.save, self.switchTotalDisplay)
dialog.cancel();
});
dialog.cancel()
})
QUnit.module('Gradebook', (_suiteHooks) => {
let gradebook;
QUnit.module('Gradebook', _suiteHooks => {
let gradebook
QUnit.module('#updateSubmissionsFromExternal', (hooks) => {
QUnit.module('#updateSubmissionsFromExternal', hooks => {
const columns = [
{ id: 'student', type: 'student' },
{ id: 'assignment_232', type: 'assignment' },
{ id: 'total_grade', type: 'total_grade' },
{ id: 'assignment_group_12', type: 'assignment' }
];
{id: 'student', type: 'student'},
{id: 'assignment_232', type: 'assignment'},
{id: 'total_grade', type: 'total_grade'},
{id: 'assignment_group_12', type: 'assignment'}
]
hooks.beforeEach(() => {
gradebook = createGradebook();
gradebook = createGradebook()
gradebook.students = {
1101: { id: '1101', row: '1', assignment_201: {}, assignment_202: {} },
1102: { id: '1102', row: '2', assignment_201: {} }
};
1101: {id: '1101', row: '1', assignment_201: {}, assignment_202: {}},
1102: {id: '1102', row: '2', assignment_201: {}}
}
gradebook.assignments = []
gradebook.submissionStateMap = {
setSubmissionCellState () {},
getSubmissionState () { return { locked: false } }
};
setSubmissionCellState() {},
getSubmissionState() {
return {locked: false}
}
}
sinon.stub(gradebook, 'updateAssignmentVisibilities');
sinon.stub(gradebook, 'updateSubmission');
sinon.stub(gradebook, 'calculateStudentGrade');
sinon.stub(gradebook, 'updateRowTotals');
sinon.stub(gradebook, 'updateAssignmentVisibilities')
sinon.stub(gradebook, 'updateSubmission')
sinon.stub(gradebook, 'calculateStudentGrade')
sinon.stub(gradebook, 'updateRowTotals')
gradebook.grid = {
getActiveCell () {},
getColumns () { return columns },
getActiveCell() {},
getColumns() {
return columns
},
updateCell: sinon.stub(),
getActiveCellNode: sinon.stub(),
};
});
getActiveCellNode: sinon.stub()
}
})
test('ignores submissions for students not currently loaded', () => {
const submissions = [
{ assignment_id: '201', user_id: '1101', score: 10, assignment_visible: true },
{ assignment_id: '201', user_id: '1103', score: 9, assignment_visible: true },
{ assignment_id: '201', user_id: '1102', score: 8, assignment_visible: true }
];
gradebook.updateSubmissionsFromExternal(submissions);
{assignment_id: '201', user_id: '1101', score: 10, assignment_visible: true},
{assignment_id: '201', user_id: '1103', score: 9, assignment_visible: true},
{assignment_id: '201', user_id: '1102', score: 8, assignment_visible: true}
]
gradebook.updateSubmissionsFromExternal(submissions)
const rowsUpdated = gradebook.updateRowTotals.getCalls().map((stubCall) => stubCall.args[0]);
deepEqual(rowsUpdated, ['1', '2']);
});
});
});
const rowsUpdated = gradebook.updateRowTotals.getCalls().map(stubCall => stubCall.args[0])
deepEqual(rowsUpdated, ['1', '2'])
})
})
})

View File

@ -18,53 +18,67 @@
import ScoreToGradeHelper from 'jsx/gradebook/shared/helpers/ScoreToGradeHelper'
QUnit.module('ScoreToGradeHelper#scoreToGrade');
QUnit.module('ScoreToGradeHelper#scoreToGrade')
test('formats score as empty string when score is null', function () {
const grade = ScoreToGradeHelper.scoreToGrade(null);
equal(grade, '');
});
test('formats score as empty string when score is null', function() {
const grade = ScoreToGradeHelper.scoreToGrade(null)
equal(grade, '')
})
test('formats score as points when grading_type is "points"', function () {
const grade = ScoreToGradeHelper.scoreToGrade(12.34, { grading_type: 'points' });
equal(grade, '12.34');
});
test('formats score as points when grading_type is "points"', function() {
const grade = ScoreToGradeHelper.scoreToGrade(12.34, {grading_type: 'points'})
equal(grade, '12.34')
})
test('formats score as percentage when grading_type is "percent"', function () {
const grade = ScoreToGradeHelper.scoreToGrade(12.34, { grading_type: 'percent', points_possible: 50 });
equal(grade, '24.68%');
});
test('formats score as percentage when grading_type is "percent"', function() {
const grade = ScoreToGradeHelper.scoreToGrade(12.34, {
grading_type: 'percent',
points_possible: 50
})
equal(grade, '24.68%')
})
test('formats score as empty string when grading_type is "percent" and assignment has no points_possible', function () {
const grade = ScoreToGradeHelper.scoreToGrade(12.34, { grading_type: 'percent', points_possible: 0 });
equal(grade, '');
});
test('formats score as empty string when grading_type is "percent" and assignment has no points_possible', function() {
const grade = ScoreToGradeHelper.scoreToGrade(12.34, {
grading_type: 'percent',
points_possible: 0
})
equal(grade, '')
})
test('formats score as "complete" when grading_type is "pass_fail" and score is nonzero"', function () {
const grade = ScoreToGradeHelper.scoreToGrade(12.34, { grading_type: 'pass_fail' });
equal(grade, 'complete');
});
test('formats score as "complete" when grading_type is "pass_fail" and score is nonzero"', function() {
const grade = ScoreToGradeHelper.scoreToGrade(12.34, {grading_type: 'pass_fail'})
equal(grade, 'complete')
})
test('formats score as "incomplete" when grading_type is "pass_fail" and score is zero"', function () {
const grade = ScoreToGradeHelper.scoreToGrade(0, { grading_type: 'pass_fail' });
equal(grade, 'incomplete');
});
test('formats score as "incomplete" when grading_type is "pass_fail" and score is zero"', function() {
const grade = ScoreToGradeHelper.scoreToGrade(0, {grading_type: 'pass_fail'})
equal(grade, 'incomplete')
})
test('formats score as empty string when grading_type is "letter_grade" and no gradingScheme given', function () {
const grade = ScoreToGradeHelper.scoreToGrade(12.34, { grading_type: 'letter_grade', points_possible: 10 });
equal(grade, '');
});
test('formats score as empty string when grading_type is "letter_grade" and no gradingScheme given', function() {
const grade = ScoreToGradeHelper.scoreToGrade(12.34, {
grading_type: 'letter_grade',
points_possible: 10
})
equal(grade, '')
})
test('formats score as empty string when grading_type is "letter_grade" and assignment has no points_possible', function () {
const grade = ScoreToGradeHelper.scoreToGrade(12.34, { grading_type: 'letter_grade' }, [
['A', 0.9], ['B', 0.8], ['C', 0.7], ['F', 0]
]);
equal(grade, '');
});
test('formats score as empty string when grading_type is "letter_grade" and assignment has no points_possible', function() {
const grade = ScoreToGradeHelper.scoreToGrade(12.34, {grading_type: 'letter_grade'}, [
['A', 0.9],
['B', 0.8],
['C', 0.7],
['F', 0]
])
equal(grade, '')
})
test('formats score as letter grade when grading_type is "letter_grade" and gradingScheme given', function () {
const grade = ScoreToGradeHelper.scoreToGrade(7, { grading_type: 'letter_grade', points_possible: 10 }, [
['A', 0.9], ['B', 0.8], ['C', 0.7], ['F', 0]
]);
equal(grade, 'C');
});
test('formats score as letter grade when grading_type is "letter_grade" and gradingScheme given', function() {
const grade = ScoreToGradeHelper.scoreToGrade(
7,
{grading_type: 'letter_grade', points_possible: 10},
[['A', 0.9], ['B', 0.8], ['C', 0.7], ['F', 0]]
)
equal(grade, 'C')
})