refactor gradeSort and assignment group column header

This is prework for CNVS-34245.

refs CNVS-34245

test plan:
* smoke test assignment grade sorting in Gradezilla
* smoke test assignment group column header

Change-Id: I4317a67780961af12f948ab69ef4be223b5667c2
Reviewed-on: https://gerrit.instructure.com/105835
Tested-by: Jenkins
Reviewed-by: Brian Park <brian@siimpl.io>
Reviewed-by: Neil Gupta <ngupta@instructure.com>
QA-Review: Neil Gupta <ngupta@instructure.com>
Product-Review: Keith T. Garner <kgarner@instructure.com>
This commit is contained in:
Jeremy Neander 2017-03-20 15:16:55 -05:00
parent 22861782a4
commit 79f3cb1fbf
4 changed files with 302 additions and 166 deletions

View File

@ -1508,28 +1508,32 @@ define [
@addDroppedClass(student)
@grid?.invalidate()
getStudentGradeForColumn: (student, field) =>
student[field] || { score: null, possible: 0 }
getGradeAsPercent: (grade) =>
if grade.possible > 0
(grade.score || 0) / grade.possible
else
null
localeSort: (a, b) ->
natcompare.strings(a || '', b || '')
gradeSort: (a, b, field, asc) =>
scoreForSorting = (obj) =>
percent = (obj) ->
if obj[field].possible > 0
obj[field].score / obj[field].possible
else
null
scoreForSorting = (student) =>
grade = @getStudentGradeForColumn(student, field)
switch
when field == "total_grade"
if @options.show_total_grade_as_points
obj[field].score
grade.score
else
percent(obj)
@getGradeAsPercent(grade)
when field.match /^assignment_group/
percent(obj)
@getGradeAsPercent(grade)
else
# TODO: support assignment grading types
obj[field].score
grade.score
NumberCompare(scoreForSorting(a), scoreForSorting(b), descending: !asc)

View File

@ -1,55 +1,80 @@
/*
* Copyright (C) 2017 Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from 'react'
import IconMoreSolid from 'instructure-icons/react/Solid/IconMoreSolid'
import { MenuItem } from 'instructure-ui/Menu'
import PopoverMenu from 'instructure-ui/PopoverMenu'
import ScreenReaderContent from 'instructure-ui/ScreenReaderContent'
import Typography from 'instructure-ui/Typography'
import I18n from 'i18n!gradebook'
// TODO: remove this rule when this component begins using internal state
/* eslint-disable react/prefer-stateless-function */
class AssignmentGroupColumnHeader extends React.Component {
static propTypes = {
assignmentGroup: React.PropTypes.shape({
name: React.PropTypes.string.isRequired,
groupWeight: React.PropTypes.number
}).isRequired,
weightedGroups: React.PropTypes.bool.isRequired
};
const { bool, number, shape, string } = React.PropTypes;
renderWeight () {
if (!this.props.weightedGroups) return '';
// TODO: remove this rule when this component begins using internal state
/* eslint-disable react/prefer-stateless-function */
const weightValue = this.props.assignmentGroup.groupWeight || 0;
function renderTrigger (assignmentGroup) {
return (
<span className="Gradebook__ColumnHeaderAction">
<Typography weight="bold" fontStyle="normal" size="large" color="brand">
<IconMoreSolid title={I18n.t('%{name} Options', { name: assignmentGroup.name })} />
</Typography>
</span>
);
}
function renderAssignmentGroupWeight (assignmentGroup, weightedGroups) {
if (!weightedGroups) {
return '';
}
const weightValue = assignmentGroup.groupWeight || 0;
const weightStr = I18n.n(weightValue, { precision: 2, percentage: true });
const weightDesc = I18n.t('%{weight} of grade', { weight: weightStr });
return (
<Typography weight="normal" fontStyle="normal" size="x-small">
{weightDesc}
{ I18n.t('%{weight} of grade', { weight: weightStr }) }
</Typography>
);
}
}
class AssignmentGroupColumnHeader extends React.Component {
static propTypes = {
assignmentGroup: shape({
name: string.isRequired,
groupWeight: number
}).isRequired,
weightedGroups: bool.isRequired
};
render () {
const optionsTitle = I18n.t('%{name} Options', { name: this.props.assignmentGroup.name });
const { assignmentGroup, weightedGroups } = this.props;
return (
<div className="Gradebook__ColumnHeaderContent">
<span className="Gradebook__ColumnHeaderDetail">
{this.props.assignmentGroup.name}
{this.renderWeight()}
<span>{ this.props.assignmentGroup.name }</span>
{ renderAssignmentGroupWeight(assignmentGroup, weightedGroups) }
</span>
<PopoverMenu
zIndex="9999"
trigger={
<span className="Gradebook__ColumnHeaderAction">
<Typography weight="bold" fontStyle="normal" size="large" color="brand">
<IconMoreSolid title={optionsTitle} />
</Typography>
</span>
}
trigger={renderTrigger(this.props.assignmentGroup)}
>
<MenuItem>Placeholder Item 1</MenuItem>
<MenuItem>Placeholder Item 2</MenuItem>
@ -58,6 +83,6 @@ import I18n from 'i18n!gradebook'
</div>
);
}
}
}
export default AssignmentGroupColumnHeader

View File

@ -20,6 +20,7 @@ import _ from 'underscore'
import React from 'react'
import ReactDOM from 'react-dom'
import natcompare from 'compiled/util/natcompare'
import round from 'compiled/util/round'
import fakeENV from 'helpers/fakeENV'
import GradeCalculatorSpecHelper from 'spec/jsx/gradebook/GradeCalculatorSpecHelper'
import SubmissionDetailsDialog from 'compiled/SubmissionDetailsDialog'
@ -264,6 +265,43 @@ test('does not calculate when the student is not initialized', function () {
notOk(CourseGradeCalculator.calculate.called);
});
QUnit.module('Gradebook#getStudentGradeForColumn');
test('returns the grade stored on the student for the column id', function () {
const student = { total_grade: { score: 5, possible: 10 } };
const grade = createGradebook().getStudentGradeForColumn(student, 'total_grade');
equal(grade, student.total_grade);
});
test('returns an empty grade when the student has no grade for the column id', function () {
const student = { total_grade: undefined };
const grade = createGradebook().getStudentGradeForColumn(student, 'total_grade');
strictEqual(grade.score, null, 'grade has a null score');
strictEqual(grade.possible, 0, 'grade has no points possible');
});
QUnit.module('Gradebook#getGradeAsPercent');
test('returns a percent for a grade with points possible', function () {
const percent = createGradebook().getGradeAsPercent({ score: 5, possible: 10 });
equal(percent, 0.5);
});
test('returns null for a grade with no points possible', function () {
const percent = createGradebook().getGradeAsPercent({ score: 5, possible: 0 });
strictEqual(percent, null);
});
test('returns 0 for a grade with a null score', function () {
const percent = createGradebook().getGradeAsPercent({ score: null, possible: 10 });
strictEqual(percent, 0);
});
test('returns 0 for a grade with an undefined score', function () {
const percent = createGradebook().getGradeAsPercent({ score: undefined, possible: 10 });
strictEqual(percent, 0);
});
QUnit.module('Gradebook#localeSort');
test('delegates to natcompare.strings', function () {
@ -280,60 +318,106 @@ test('substitutes falsy args with empty string', function () {
deepEqual(natcompare.strings.getCall(0).args, ['', '']);
});
QUnit.module('Gradebook#gradeSort');
QUnit.module('Gradebook#gradeSort by an assignment', {
setup () {
this.studentA = { assignment_201: { score: 10, possible: 20 } };
this.studentB = { assignment_201: { score: 6, possible: 10 } };
}
});
test('gradeSort - total_grade', function () {
const gradeSort = function (showTotalGradeAsPoints, a, b, field, asc = true) {
return Gradebook.prototype.gradeSort.call({
options: {
show_total_grade_as_points: showTotalGradeAsPoints
test('always sorts by score', function () {
const gradebook = createGradebook({ show_total_grade_as_points: true });
const comparison = gradebook.gradeSort(this.studentA, this.studentB, 'assignment_201', true);
// a positive value indicates reversing the order of inputs
equal(comparison, 4, 'studentA with the higher score is ordered second');
});
test('optionally sorts in descending order', function () {
const gradebook = createGradebook({ show_total_grade_as_points: true });
const comparison = gradebook.gradeSort(this.studentA, this.studentB, 'assignment_201', false);
// a negative value indicates preserving the order of inputs
equal(comparison, -4, 'studentA with the higher score is ordered first');
});
QUnit.module('Gradebook#gradeSort by an assignment group', {
setup () {
this.studentA = { assignment_group_301: { score: 10, possible: 20 } };
this.studentB = { assignment_group_301: { score: 6, possible: 10 } };
}
}, a, b, field, asc);
};
ok(gradeSort(false, {
total_grade: {
score: 10,
possible: 20
});
test('always sorts by percent', function () {
const gradebook = createGradebook({ show_total_grade_as_points: false });
const comparison = gradebook.gradeSort(this.studentA, this.studentB, 'assignment_group_301', true);
// a negative value indicates preserving the order of inputs
equal(round(comparison, 1), -0.1, 'studentB with the higher percent is ordered second');
});
test('optionally sorts in descending order', function () {
const gradebook = createGradebook({ show_total_grade_as_points: true });
const comparison = gradebook.gradeSort(this.studentA, this.studentB, 'assignment_group_301', false);
// a positive value indicates reversing the order of inputs
equal(round(comparison, 1), 0.1, 'studentB with the higher percent is ordered first');
});
test('sorts grades with no points possible at lowest priority', function () {
this.studentA.assignment_group_301.possible = 0;
const gradebook = createGradebook({ show_total_grade_as_points: false });
const comparison = gradebook.gradeSort(this.studentA, this.studentB, 'assignment_group_301', true);
// a value of 1 indicates reversing the order of inputs
equal(comparison, 1, 'studentA with no points possible is ordered second');
});
test('sorts grades with no points possible at lowest priority in descending order', function () {
this.studentA.assignment_group_301.possible = 0;
const gradebook = createGradebook({ show_total_grade_as_points: false });
const comparison = gradebook.gradeSort(this.studentA, this.studentB, 'assignment_group_301', false);
// a value of 1 indicates reversing the order of inputs
equal(comparison, 1, 'studentA with no points possible is ordered second');
});
QUnit.module('Gradebook#gradeSort by "total_grade"', {
setup () {
this.studentA = { total_grade: { score: 10, possible: 20 } };
this.studentB = { total_grade: { score: 6, possible: 10 } };
}
}, {
total_grade: {
score: 5,
possible: 10
}
}, 'total_grade') === 0, 'total_grade sorts by percent (normally)');
ok(gradeSort(true, {
total_grade: {
score: 10,
possible: 20
}
}, {
total_grade: {
score: 5,
possible: 10
}
}, 'total_grade') > 0, 'total_grade sorts by score when if show_total_grade_as_points');
ok(gradeSort(true, {
assignment_group_1: {
score: 10,
possible: 20
}
}, {
assignment_group_1: {
score: 5,
possible: 10
}
}, 'assignment_group_1') === 0, 'assignment groups are always sorted by percent');
ok(gradeSort(false, {
assignment1: {
score: 5,
possible: 10
}
}, {
assignment1: {
score: 10,
possible: 20
}
}, 'assignment1') < 0, 'other fields are sorted by score');
});
test('sorts by percent when not showing total grade as points', function () {
const gradebook = createGradebook({ show_total_grade_as_points: false });
const comparison = gradebook.gradeSort(this.studentA, this.studentB, 'total_grade', true);
// a negative value indicates preserving the order of inputs
equal(round(comparison, 1), -0.1, 'studentB with the higher percent is ordered second');
});
test('sorts percent grades with no points possible at lowest priority', function () {
this.studentA.total_grade.possible = 0;
const gradebook = createGradebook({ show_total_grade_as_points: false });
const comparison = gradebook.gradeSort(this.studentA, this.studentB, 'total_grade', true);
// a value of 1 indicates reversing the order of inputs
equal(comparison, 1, 'studentA with no points possible is ordered second');
});
test('sorts percent grades with no points possible at lowest priority in descending order', function () {
this.studentA.total_grade.possible = 0;
const gradebook = createGradebook({ show_total_grade_as_points: false });
const comparison = gradebook.gradeSort(this.studentA, this.studentB, 'total_grade', false);
// a value of 1 indicates reversing the order of inputs
equal(comparison, 1, 'studentA with no points possible is ordered second');
});
test('sorts by score when showing total grade as points', function () {
const gradebook = createGradebook({ show_total_grade_as_points: true });
const comparison = gradebook.gradeSort(this.studentA, this.studentB, 'total_grade', true);
// a positive value indicates reversing the order of inputs
equal(comparison, 4, 'studentA with the higher score is ordered second');
});
test('optionally sorts in descending order', function () {
const gradebook = createGradebook({ show_total_grade_as_points: true });
const comparison = gradebook.gradeSort(this.studentA, this.studentB, 'total_grade', false);
// a negative value indicates preserving the order of inputs
equal(comparison, -4, 'studentA with the higher score is ordered first');
});
QUnit.module('Gradebook#hideAggregateColumns', {

View File

@ -1,58 +1,81 @@
define([
'react',
'react-addons-test-utils',
'enzyme',
'jsx/gradezilla/default_gradebook/components/AssignmentGroupColumnHeader'
], (React, TestUtils, { mount }, AssignmentGroupColumnHeader) => {
QUnit.module('AssignmentGroupColumnHeader - base behavior', {
setup () {
this.assignmentGroup = {
name: 'Assignment Group 1',
groupWeight: 42.5
};
/*
* Copyright (C) 2017 Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
this.renderOutput = mount(<AssignmentGroupColumnHeader assignmentGroup={this.assignmentGroup} weightedGroups />);
import React from 'react'
import { mount } from 'enzyme'
import AssignmentGroupColumnHeader from 'jsx/gradezilla/default_gradebook/components/AssignmentGroupColumnHeader'
function createExampleProps () {
return {
assignmentGroup: {
groupWeight: 42.5,
name: 'Assignment Group 1'
},
weightedGroups: true
};
}
function mountComponent (props) {
return mount(<AssignmentGroupColumnHeader {...props} />);
}
QUnit.module('AssignmentGroupColumnHeader - base behavior', {
setup () {
const props = createExampleProps();
this.wrapper = mountComponent(props);
},
teardown () {
this.renderOutput.unmount();
this.wrapper.unmount();
}
});
test('renders the assignment group name', function () {
const actualElements = this.renderOutput.find('.Gradebook__ColumnHeaderDetail');
equal(actualElements.props().children[0], 'Assignment Group 1');
});
test('renders the assignment groupWeight percentage', function () {
const actualElements = this.renderOutput.find('.Gradebook__ColumnHeaderDetail Typography');
equal(actualElements.props().children, '42.50% of grade');
});
QUnit.module('AssignmentGroupColumnHeader - non-standard assignment group', {
setup () {
this.assignmentGroup = {
name: 'Assignment Group 1',
groupWeight: 42.5
};
},
});
test('renders 0% as the groupWeight percentage when weightedGroups is true but groupWeight is 0', function () {
this.assignmentGroup.groupWeight = 0;
const renderOutput = mount(<AssignmentGroupColumnHeader assignmentGroup={this.assignmentGroup} weightedGroups />);
const actualElements = renderOutput.find('.Gradebook__ColumnHeaderDetail Typography');
equal(actualElements.props().children, '0.00% of grade');
});
test('does not render the groupWeight percentage when weightedGroups is false', function () {
const renderOutput = mount(<AssignmentGroupColumnHeader assignmentGroup={this.assignmentGroup} weightedGroups={false} />);
const actualElements = renderOutput.find('.Gradebook__ColumnHeaderDetail Typography');
equal(actualElements.length, 0);
});
});
test('renders the assignment group name', function () {
const assignmentGroupName = this.wrapper.find('.Gradebook__ColumnHeaderDetail').childAt(0);
equal(assignmentGroupName.text(), 'Assignment Group 1');
});
test('renders the assignment groupWeight percentage', function () {
const groupWeight = this.wrapper.find('.Gradebook__ColumnHeaderDetail').childAt(1);
equal(groupWeight.text(), '42.50% of grade');
});
QUnit.module('AssignmentGroupColumnHeader - non-standard assignment group', {
setup () {
this.props = createExampleProps();
},
});
test('renders 0% as the groupWeight percentage when weightedGroups is true but groupWeight is 0', function () {
this.props.assignmentGroup.groupWeight = 0;
const wrapper = mountComponent(this.props);
const groupWeight = wrapper.find('.Gradebook__ColumnHeaderDetail').childAt(1);
equal(groupWeight.text(), '0.00% of grade');
});
test('does not render the groupWeight percentage when weightedGroups is false', function () {
this.props.weightedGroups = false;
const wrapper = mountComponent(this.props);
const headerDetails = wrapper.find('.Gradebook__ColumnHeaderDetail').children();
equal(headerDetails.length, 1, 'only the assignment group name is visible');
equal(headerDetails.text(), 'Assignment Group 1');
});