convert score to letter grade correctly

closes EVAL-3896
flag=none

Test Plan:
- Create a points based grading scheme with a range of
A: 15 <= 13
B: < 13 <= 10
C: < 10 <= 7
D: < 7 <= 0
- Set a letter graded assignment ot that scheme and ensure a 13 is an A
and a 12.999 is a B and a 10 is a B and a 9.999 is a C and a 7 is a C
- Ensure this works for all places grades are displayed like:
    -gradebook
    -individual gradebook
    -student grades page
    -etc

Change-Id: Icdf901c42c8c9ce45abe2943e6a073045fca56db
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/343681
QA-Review: Rohan Chugh <rohan.chugh@instructure.com>
Product-Review: Melissa Kruger <melissa.kruger@instructure.com>
Reviewed-by: Derek Williams <derek.williams@instructure.com>
Reviewed-by: Christopher Soto <christopher.soto@instructure.com>
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
This commit is contained in:
Kai Bjorkman 2024-03-25 12:26:03 -06:00
parent 5c4848a1a9
commit f44c9b6896
40 changed files with 274 additions and 61 deletions

View File

@ -83,7 +83,8 @@ class AssignmentsController < ApplicationController
newquizzes_on_quiz_page: @context.root_account.feature_enabled?(:newquizzes_on_quiz_page),
show_additional_speed_grader_link: Account.site_admin.feature_enabled?(:additional_speedgrader_links),
},
grading_scheme: @context.grading_standard_or_default.data
grading_scheme: @context.grading_standard_or_default.data,
points_based: @context.grading_standard_or_default.points_based?,
}
set_default_tool_env!(@context, hash)
@ -148,7 +149,8 @@ class AssignmentsController < ApplicationController
peer_display_name: @assignment.anonymous_peer_reviews? ? I18n.t("Anonymous student") : submission&.user&.name,
originality_reports_for_a2_enabled: Account.site_admin.feature_enabled?(:originality_reports_for_a2),
restrict_quantitative_data: @assignment.restrict_quantitative_data?(@current_user),
grading_scheme: @context.grading_standard_or_default.data
grading_scheme: @context.grading_standard_or_default.data,
points_based: @context.grading_standard_or_default.points_based?,
})
if peer_review_mode_enabled

View File

@ -2391,6 +2391,7 @@ class CoursesController < ApplicationController
TAB_CONTENT_ONLY: embed_mode,
SHOW_IMMERSIVE_READER: show_immersive_reader?,
GRADING_SCHEME: @context.grading_standard_or_default.data,
POINTS_BASED: @context.grading_standard_or_default.points_based?,
RESTRICT_QUANTITATIVE_DATA: @context.restrict_quantitative_data?(@current_user)
)

View File

@ -279,6 +279,7 @@ class UsersController < ApplicationController
end
grade_data[:restrict_quantitative_data] = enrollment.course.restrict_quantitative_data?(@current_user)
grade_data[:grading_scheme] = enrollment.course.grading_standard_or_default.data
grade_data[:points_based_grading_scheme] = enrollment.course.grading_standard_or_default.points_based?
render json: grade_data
end

View File

@ -194,6 +194,9 @@ class GradingStandard < ActiveRecord::Base
score = 0 if score < 0
# assign the highest grade whose min cutoff is less than the score
# if score is less than all scheme cutoffs, assign the lowest grade
if points_based
score = score.round(2) # round to 2 decimal places because points based grading schemes lower bounds are rounded to 2 decimal places
end
score = BigDecimal(score.to_s) # Cast this to a BigDecimal too or comparisons get wonky
ordered_scheme.max_by { |_, lower_bound| (score >= lower_bound * BigDecimal("100.0")) ? lower_bound : -lower_bound }[0]
end

View File

@ -169,6 +169,7 @@ module Api::V1::Course
end
hash["grading_scheme"] = course.grading_standard_or_default.data if includes.include?("grading_scheme")
hash["points_based_grading_scheme"] = course.grading_standard_or_default.points_based? if includes.include?("grading_scheme")
hash["restrict_quantitative_data"] = course.restrict_quantitative_data?(user) if includes.include?("restrict_quantitative_data")
# return hash from the block for additional processing in Api::V1::CourseJson

View File

@ -17,7 +17,7 @@
*/
import {describe, test, expect} from '@jest/globals'
import {scoreToGrade, scoreToLetterGrade} from '../index.js'
import {scoreToGrade, scoreToLetterGrade} from '../index'
describe('index', () => {
describe('scoreToGrade', () => {
@ -81,6 +81,20 @@ describe('index', () => {
expect(scoreToGrade(0, gradingScheme)).toBe('M')
expect(scoreToGrade(-100, gradingScheme)).toBe('M')
})
test('rounds point based grading schemes to appropriate precision to ensure bottom limit is ', () => {
const gradingScheme = [
{name: 'A', value: 0.8667},
{name: 'B', value: 0.6667},
{name: 'C', value: 0.4667},
{name: 'D', value: 0},
]
expect(scoreToGrade(86.6666666666667, gradingScheme, true)).toBe('A')
expect(scoreToGrade(66.6666666666667, gradingScheme, true)).toBe('B')
expect(scoreToGrade(46.6666666666667, gradingScheme, true)).toBe('C')
expect(scoreToGrade(45, gradingScheme, true)).toBe('D')
})
})
describe('scoreToLetterGrade', () => {
@ -129,5 +143,19 @@ describe('index', () => {
expect(scoreToLetterGrade(90, gradingScheme)).toBe('A')
expect(scoreToLetterGrade(89.999, gradingScheme)).toBe('B+')
})
test('rounds point based grading schemes to appropriate precision to ensure bottom limit is ', () => {
const gradingScheme = [
{name: 'A', value: 0.8667},
{name: 'B', value: 0.6667},
{name: 'C', value: 0.4667},
{name: 'D', value: 0},
]
expect(scoreToLetterGrade(86.6666666666667, gradingScheme, true)).toBe('A')
expect(scoreToLetterGrade(66.6666666666667, gradingScheme, true)).toBe('B')
expect(scoreToLetterGrade(46.6666666666667, gradingScheme, true)).toBe('C')
expect(scoreToLetterGrade(45, gradingScheme, true)).toBe('D')
})
})
})

View File

@ -17,7 +17,7 @@
*/
// @ts-ignore
import Big from 'big.js';
import Big from 'big.js'
/**
* @typedef {[string, number]} GradingStandard
@ -30,44 +30,50 @@ import Big from 'big.js';
*/
/**
* @deprecated Use scoreToLetterGrade(score: number, gradingSchemeDataRows: GradingSchemeDataRow[]) instead, which takes
* @deprecated Use scoreToLetterGrade(score: number, gradingSchemeDataRows: GradingSchemeDataRow[], points_based) instead, which takes
* a more reasonably typed object model than the 2d array that this function takes in for gradingScheme data rows.
* @param {number} score
* @param {GradingStandard[]} gradingSchemes
* @param {boolean} pointsBased
* @returns {?string}
*/
export function scoreToGrade(score, gradingSchemes) {
export function scoreToGrade(score, gradingSchemes, pointsBased = false) {
// Because scoreToGrade is being used in a non typescript file, ui/features/grade_summary/jquery/index.js,
// score can be NaN despite its type being declared as a number
if (typeof score !== 'number' || Number.isNaN(score) || gradingSchemes == null) {
return null;
return null
}
// convert deprecated 2d array format to newer GradingSchemeDataRow[] format
const gradingSchemeDataRows = gradingSchemes.map(row => ({ name: row[0], value: row[1] }));
return scoreToLetterGrade(score, gradingSchemeDataRows);
const gradingSchemeDataRows = gradingSchemes.map(row => ({name: row[0], value: row[1]}))
return scoreToLetterGrade(score, gradingSchemeDataRows, pointsBased)
}
/**
* @param {number} score
* @param {GradingSchemeDataRow[]} gradingSchemeDataRows
* @param {boolean} pointsBased
* @returns {string}
*/
export function scoreToLetterGrade(score, gradingSchemeDataRows) {
export function scoreToLetterGrade(score, gradingSchemeDataRows, pointsBased = false) {
// Because scoreToGrade is being used in a non typescript file, ui/features/grade_summary/jquery/index.js,
// score can be NaN despite its type being declared as a number
if (typeof score !== 'number' || Number.isNaN(score) || gradingSchemeDataRows == null) {
return null;
return null
}
const roundedScore = parseFloat(Big(score).round(4));
const scoreWithLowerBound = Math.max(roundedScore, 0);
const roundedScore = pointsBased
? parseFloat(Big(score).round(2)) // round to 2 decimal places because points based grading schemes lower bounds are rounded to 2 decimal places
: parseFloat(Big(score).round(4))
const scoreWithLowerBound = Math.max(roundedScore, 0)
const letter = gradingSchemeDataRows.find((row, i) => {
const schemeScore = (row.value * 100).toPrecision(4);
return scoreWithLowerBound >= parseFloat(schemeScore) || i === gradingSchemeDataRows.length - 1;
});
const schemeScore = (row.value * 100).toPrecision(4)
return scoreWithLowerBound >= parseFloat(schemeScore) || i === gradingSchemeDataRows.length - 1
})
if (!letter) {
throw new Error('grading scheme not found');
throw new Error('grading scheme not found')
}
return letter.name;
return letter.name
}

View File

@ -266,6 +266,17 @@ describe GradingStandard do
expect(standard.score_to_grade(-100)).to eql("M")
end
it "computes correct grades for points based grading scemes" do
input = [["A", 0.8667], ["B", 0.6667], ["C", 0.4667], ["D", 0]]
standard = GradingStandard.new
standard.data = input
standard.points_based = true
expect(standard.score_to_grade(86.66666666667)).to eql("A")
expect(standard.score_to_grade(66.66666666667)).to eql("B")
expect(standard.score_to_grade(46.66666666667)).to eql("C")
expect(standard.score_to_grade(0)).to eql("D")
end
it "assigns the lowest grade to below-scale scores" do
input = [["A", 0.90], ["B", 0.80], ["C", 0.70], ["D", 0.60], ["E", 0.50]]
standard = GradingStandard.new

View File

@ -797,13 +797,14 @@ export default AssignmentListItemView = (function () {
if (json.pointsPossible === 0 && json.submission.score < 0) {
grade = json.submission.score
} else if (json.pointsPossible === 0 && json.submission.score > 0) {
grade = scoreToGrade(100, ENV.grading_scheme)
grade = scoreToGrade(100, ENV.grading_scheme, ENV.points_based)
} else if (json.pointsPossible === 0 && json.submission.score === 0) {
grade = 'complete'
} else {
grade = scoreToGrade(
scoreToPercentage(json.submission.score, json.pointsPossible),
ENV.grading_scheme
ENV.grading_scheme,
ENV.points_based
)
}
}

View File

@ -109,6 +109,7 @@ export default ({
score: ENV.restrict_quantitative_data && submission.score != null ? submission.score : null,
restrict_quantitative_data: ENV.restrict_quantitative_data,
grading_scheme: ENV.grading_scheme,
points_based_grading_scheme: ENV.points_based,
})
return (

View File

@ -96,6 +96,7 @@ export default function PointsDisplay(props) {
formatType: 'points_out_of_fraction',
restrict_quantitative_data: ENV.restrict_quantitative_data,
grading_scheme: ENV.grading_scheme,
points_based_grading_scheme: ENV.points_based,
})
if (

View File

@ -60,7 +60,9 @@ export default function RowScore({gradingScheme, name, possible, score, weight}:
const letterGradeScore = isPercentInvalid
? '-'
: gradingScheme
? GradeFormatHelper.replaceDashWithMinus(getLetterGrade(possible, score, gradingScheme.data))
? GradeFormatHelper.replaceDashWithMinus(
getLetterGrade(possible, score, gradingScheme.data, gradingScheme.pointsBased)
)
: '-'
const weightText = weight ? I18n.n(weight, {percentage: true}) : '-'

View File

@ -211,7 +211,7 @@ export default function StudentInformation({
const letterGradeText = gradingStandard
? ` - ${GradeFormatHelper.replaceDashWithMinus(
getLetterGrade(possible, score, gradingStandard)
getLetterGrade(possible, score, gradingStandard, gradingStandardPointsBased)
)}`
: ''

View File

@ -406,14 +406,15 @@ export function scoreToScaledPoints(score: number, pointsPossible: number, scali
export function getLetterGrade(
possible?: number,
score?: number,
gradingStadards?: GradingStandard[] | null
gradingStandards?: GradingStandard[] | null,
pointsBased?: boolean
) {
if (!gradingStadards || !gradingStadards.length || !possible || !score) {
if (!gradingStandards || !gradingStandards.length || !possible || !score) {
return '-'
}
const rawPercentage = scoreToPercentage(score, possible)
const percentage = parseFloat(Number(rawPercentage).toPrecision(4))
return scoreToGrade(percentage, gradingStadards)
return scoreToGrade(percentage, gradingStandards, pointsBased)
}
type CalculateGradesForUserProps = {

View File

@ -474,7 +474,12 @@ function calculateTotals(calculatedGrades, currentOrFinal, groupWeightingScheme)
: calculatePercentGrade(finalScore, finalPossible)
const grading_scheme = ENV.course_active_grading_scheme?.data
const letterGrade = scoreToLetterGrade(scoreToUse, grading_scheme) || I18n.t('N/A')
const letterGrade =
scoreToLetterGrade(
scoreToUse,
grading_scheme,
ENV.course_active_grading_scheme?.points_based
) || I18n.t('N/A')
$('.final_grade .letter_grade').text(GradeFormatHelper.replaceDashWithMinus(letterGrade))
}

View File

@ -3581,6 +3581,7 @@ class Gradebook extends React.Component<GradebookProps, GradebookState> {
!!(submissionState != null ? submissionState.locked : undefined) || student.isConcluded
),
gradingScheme: this.getAssignmentGradingScheme(assignmentId)?.data || null,
pointsBasedGradingScheme: this.getAssignmentGradingScheme(assignmentId)?.pointsBased || false,
isFirstAssignment,
isInOtherGradingPeriod: !!(submissionState != null
? submissionState.inOtherGradingPeriod

View File

@ -58,6 +58,8 @@ type Props = {
gradingScheme: [name: string, value: number][]
pointsBasedGradingScheme: boolean
onGradeSubmission: (submission: Submission, grade: string) => void
onToggleSubmissionTrayOpen: (assignmentId: string, userId: string) => void
@ -228,6 +230,7 @@ export default class AssignmentRowCell extends Component<Props> {
enterGradesAs={this.props.enterGradesAs}
disabled={this.props.submissionIsUpdating}
gradingScheme={this.props.gradingScheme}
pointsBasedGradingScheme={this.props.pointsBasedGradingScheme}
pendingGradeInfo={this.props.pendingGradeInfo}
ref={this.bindGradeInput}
submission={this.props.submission}

View File

@ -86,6 +86,8 @@ export default class AssignmentRowCellPropFactory {
gradeIsEditable: this.gradebook.isGradeEditable(student.id, assignment.id),
gradeIsVisible: this.gradebook.isGradeVisible(student.id, assignment.id),
gradingScheme: this.gradebook.getAssignmentGradingScheme(assignment.id)?.data,
pointsBasedGradingScheme: this.gradebook.getAssignmentGradingScheme(assignment.id)
?.pointsBased,
isSubmissionTrayOpen: isTrayOpen(this.gradebook, student, assignment),
onToggleSubmissionTrayOpen: () => {

View File

@ -36,11 +36,18 @@ const componentOverrides = {
},
}
function formatGrade(submission, assignment, gradingScheme, enterGradesAs) {
function formatGrade(
submission,
assignment,
gradingScheme,
pointsBasedGradingScheme,
enterGradesAs
) {
const formatOptions = {
defaultValue: '',
formatType: enterGradesAs,
gradingScheme,
pointsBasedGradingScheme,
pointsPossible: assignment.pointsPossible,
version: 'final',
}
@ -83,6 +90,7 @@ export default class ReadOnlyCell extends Component {
enterGradesAs: oneOf(['gradingScheme', 'passFail', 'percent', 'points']).isRequired,
gradeIsVisible: bool.isRequired,
gradingScheme: instanceOf(Array).isRequired,
pointsBasedGradingScheme: bool,
onToggleSubmissionTrayOpen: func.isRequired,
student: shape({
id: string.isRequired,
@ -141,14 +149,29 @@ export default class ReadOnlyCell extends Component {
}
render() {
const {assignment, enterGradesAs, gradeIsVisible, gradingScheme, submission} = this.props
const {
assignment,
enterGradesAs,
gradeIsVisible,
gradingScheme,
pointsBasedGradingScheme,
submission,
} = this.props
let content = ''
if (gradeIsVisible) {
if (enterGradesAs === 'passFail' && !submission.excused) {
content = renderCompleteIncompleteGrade(submission.rawGrade)
} else {
content = renderTextGrade(formatGrade(submission, assignment, gradingScheme, enterGradesAs))
content = renderTextGrade(
formatGrade(
submission,
assignment,
gradingScheme,
pointsBasedGradingScheme,
enterGradesAs
)
)
}
}

View File

@ -29,7 +29,13 @@ import {hasGradeChanged, parseTextValue} from '@canvas/grading/GradeInputHelper'
const I18n = useI18nScope('gradebook')
function formatGrade(submission, assignment, gradingScheme, pendingGradeInfo) {
function formatGrade(
submission,
assignment,
gradingScheme,
pointsBasedGradingScheme,
pendingGradeInfo
) {
if (pendingGradeInfo) {
return GradeFormatHelper.formatGradeInfo(pendingGradeInfo, {defaultValue: ''})
}
@ -38,6 +44,7 @@ function formatGrade(submission, assignment, gradingScheme, pendingGradeInfo) {
defaultValue: '',
formatType: 'gradingScheme',
gradingScheme,
pointsBasedGradingScheme,
pointsPossible: assignment.pointsPossible,
version: 'entered',
}
@ -49,6 +56,7 @@ function getGradeInfo(value, props) {
return parseTextValue(value, {
enterGradesAs: 'gradingScheme',
gradingScheme: props.gradingScheme,
pointsBasedGradingScheme: props.pointsBasedGradingScheme,
pointsPossible: props.assignment.pointsPossible,
})
}
@ -60,6 +68,7 @@ export default class GradingSchemeInput extends Component {
}).isRequired,
disabled: bool,
gradingScheme: instanceOf(Array).isRequired,
pointsBasedGradingScheme: bool,
label: element.isRequired,
menuContentRef: Menu.propTypes.menuRef,
messages: arrayOf(
@ -88,6 +97,7 @@ export default class GradingSchemeInput extends Component {
onMenuDismiss() {},
onMenuShow() {},
pendingGradeInfo: null,
pointsBasedGradingScheme: false,
}
constructor(props) {
@ -105,24 +115,50 @@ export default class GradingSchemeInput extends Component {
this.handleTextChange = this.handleTextChange.bind(this)
this.handleToggle = this.handleToggle.bind(this)
const {assignment, gradingScheme, pendingGradeInfo, submission} = props
const value = formatGrade(submission, assignment, gradingScheme, pendingGradeInfo)
const {assignment, gradingScheme, pointsBasedGradingScheme, pendingGradeInfo, submission} =
props
const value = formatGrade(
submission,
assignment,
gradingScheme,
pointsBasedGradingScheme,
pendingGradeInfo
)
this.state = {
gradeInfo: pendingGradeInfo || getGradeInfo(submission.excused ? 'EX' : value, this.props),
menuIsOpen: false,
value: formatGrade(submission, assignment, gradingScheme, pendingGradeInfo),
value: formatGrade(
submission,
assignment,
gradingScheme,
pointsBasedGradingScheme,
pendingGradeInfo
),
}
}
UNSAFE_componentWillReceiveProps(nextProps) {
if (this.textInput !== document.activeElement) {
const {assignment, gradingScheme, pendingGradeInfo, submission} = nextProps
const value = formatGrade(submission, assignment, gradingScheme, pendingGradeInfo)
const {assignment, gradingScheme, pointsBasedGradingScheme, pendingGradeInfo, submission} =
nextProps
const value = formatGrade(
submission,
assignment,
gradingScheme,
pointsBasedGradingScheme,
pendingGradeInfo
)
this.setState({
gradeInfo: pendingGradeInfo || getGradeInfo(submission.excused ? 'EX' : value, nextProps),
value: formatGrade(submission, assignment, gradingScheme, pendingGradeInfo),
value: formatGrade(
submission,
assignment,
gradingScheme,
pointsBasedGradingScheme,
pendingGradeInfo
),
})
}
}
@ -191,8 +227,13 @@ export default class GradingSchemeInput extends Component {
return this.state.value.trim() !== this.props.pendingGradeInfo.grade
}
const {assignment, gradingScheme, submission} = this.props
const formattedGrade = formatGrade(submission, assignment, gradingScheme)
const {assignment, gradingScheme, pointsBasedGradingScheme, submission} = this.props
const formattedGrade = formatGrade(
submission,
assignment,
gradingScheme,
pointsBasedGradingScheme
)
if (formattedGrade === this.state.value.trim()) {
return false

View File

@ -28,6 +28,7 @@ function formatGrade(
submission,
assignment,
gradingScheme,
pointsBasedGradingScheme,
enterGradesAs,
pendingGradeInfo: PendingGradeInfo
) {
@ -39,6 +40,7 @@ function formatGrade(
defaultValue: '',
formatType: enterGradesAs,
gradingScheme,
pointsBasedGradingScheme,
pointsPossible: assignment.pointsPossible,
version: 'entered',
}
@ -50,6 +52,7 @@ function getGradeInfo(value, props) {
return parseTextValue(value, {
enterGradesAs: props.enterGradesAs,
gradingScheme: props.gradingScheme,
pointsBasedGradingScheme: props.pointsBasedGradingScheme,
pointsPossible: props.assignment.pointsPossible,
})
}
@ -61,6 +64,7 @@ type Props = {
disabled: boolean
enterGradesAs: 'gradingScheme' | 'passFail' | 'percent' | 'points'
gradingScheme: DeprecatedGradingScheme[]
pointsBasedGradingScheme: boolean
label: React.ReactElement
messages: Array<{
text: string
@ -102,11 +106,19 @@ export default class TextGradeInput extends Component<Props, State> {
this.handleKeyDown = this.handleKeyDown.bind(this)
this.handleTextChange = this.handleTextChange.bind(this)
const {assignment, enterGradesAs, gradingScheme, pendingGradeInfo, submission} = props
const {
assignment,
enterGradesAs,
gradingScheme,
pointsBasedGradingScheme,
pendingGradeInfo,
submission,
} = props
const value = formatGrade(
submission,
assignment,
gradingScheme,
pointsBasedGradingScheme,
enterGradesAs,
pendingGradeInfo
)
@ -119,10 +131,24 @@ export default class TextGradeInput extends Component<Props, State> {
UNSAFE_componentWillReceiveProps(nextProps: Props) {
if (!this.isFocused()) {
const {assignment, enterGradesAs, gradingScheme, pendingGradeInfo, submission} = nextProps
const {
assignment,
enterGradesAs,
gradingScheme,
pointsBasedGradingScheme,
pendingGradeInfo,
submission,
} = nextProps
this.setState({
grade: formatGrade(submission, assignment, gradingScheme, enterGradesAs, pendingGradeInfo),
grade: formatGrade(
submission,
assignment,
gradingScheme,
pointsBasedGradingScheme,
enterGradesAs,
pendingGradeInfo
),
})
}
}
@ -149,8 +175,15 @@ export default class TextGradeInput extends Component<Props, State> {
return this.state.grade.trim() !== this.props.pendingGradeInfo.grade
}
const {assignment, enterGradesAs, gradingScheme, submission} = this.props
const formattedGrade = formatGrade(submission, assignment, gradingScheme, enterGradesAs)
const {assignment, enterGradesAs, gradingScheme, pointsBasedGradingScheme, submission} =
this.props
const formattedGrade = formatGrade(
submission,
assignment,
gradingScheme,
pointsBasedGradingScheme,
enterGradesAs
)
if (formattedGrade === this.state.grade.trim()) {
return false

View File

@ -41,6 +41,7 @@ type Props = {
disabled: boolean
enterGradesAs: 'gradingScheme' | 'passFail' | 'percent' | 'points'
gradingScheme: [name: string, value: number][]
pointsBasedGradingScheme: boolean
pendingGradeInfo: {
excused: boolean
grade: string

View File

@ -47,6 +47,7 @@ type Getters = {
getAssignment(assignmentId: string): ReturnType<Gradebook['getAssignment']>
getEnterGradesAsSetting(assignmentId: string): ReturnType<Gradebook['getEnterGradesAsSetting']>
getGradingSchemeData(assignmentId: string): undefined | GradingStandard[]
getPointsBasedGradingScheme(assignmentId: string): undefined | boolean
getPendingGradeInfo(submission: {
assignmentId: string
userId: string
@ -88,6 +89,7 @@ function formatGrade(submissionData: SubmissionData, assignment: Assignment, opt
const formatOptions = {
formatType: options.getEnterGradesAsSetting(assignment.id),
gradingScheme: options.getGradingSchemeData(assignment.id),
pointsBasedGradingScheme: options.getPointsBasedGradingScheme(assignment.id),
pointsPossible: assignment.points_possible,
version: 'final',
}
@ -170,6 +172,9 @@ export default class AssignmentCellFormatter {
getGradingSchemeData(assignmentId: string): undefined | GradingStandard[] {
return gradebook.getAssignmentGradingScheme(assignmentId)?.data
},
getPointsBasedGradingScheme(assignmentId: string): undefined | boolean {
return gradebook.getAssignmentGradingScheme(assignmentId)?.pointsBased
},
getPendingGradeInfo(submission: {assignmentId: string; userId: string}) {
return gradebook.getPendingGradeInfo(submission)
},

View File

@ -178,7 +178,9 @@ export default class TotalGradeCellFormatter {
let letterGrade
const scheme = this.options.getGradingStandard()
if (grade.possible && scheme) {
letterGrade = GradeFormatHelper.replaceDashWithMinus(scoreToGrade(percentage, scheme.data))
letterGrade = GradeFormatHelper.replaceDashWithMinus(
scoreToGrade(percentage, scheme.data, scheme.pointsBased)
)
}
let displayAsScaledPoints = false

View File

@ -42,7 +42,13 @@ type Message = {
}
function normalizeSubmissionGrade(props: Props) {
const {submission, assignment, enterGradesAs: formatType, gradingScheme} = props
const {
submission,
assignment,
enterGradesAs: formatType,
gradingScheme,
pointsBasedGradingScheme,
} = props
const gradeToNormalize = submission.enteredGrade
if (props.pendingGradeInfo && props.pendingGradeInfo.excused) {
@ -59,6 +65,7 @@ function normalizeSubmissionGrade(props: Props) {
defaultValue: '',
formatType,
gradingScheme,
pointsBasedGradingScheme,
pointsPossible: assignment.pointsPossible,
version: 'entered',
}
@ -132,6 +139,7 @@ type Props = {
disabled: boolean
enterGradesAs: GradeEntryMode
gradingScheme: GradingStandard[] | null
pointsBasedGradingScheme: boolean
onSubmissionUpdate: (submission: SubmissionData, gradeInfo: GradeResult) => void
pendingGradeInfo: PendingGradeInfo
submission: SubmissionData
@ -147,6 +155,7 @@ export default class GradeInput extends Component<Props, State> {
static defaultProps = {
disabled: false,
gradingScheme: null,
pointsBasedGradingScheme: false,
onSubmissionUpdate() {},
pendingGradeInfo: null,
submissionUpdating: false,
@ -208,6 +217,7 @@ export default class GradeInput extends Component<Props, State> {
const gradeInfo = parseTextValue(this.state.grade, {
enterGradesAs: this.props.enterGradesAs,
gradingScheme: this.props.gradingScheme,
pointsBasedGradingScheme: this.props.pointsBasedGradingScheme,
pointsPossible: this.props.assignment.pointsPossible,
})
@ -241,6 +251,7 @@ export default class GradeInput extends Component<Props, State> {
currentGradeInfo = parseTextValue(this.state.grade, {
enterGradesAs: this.props.enterGradesAs,
gradingScheme: this.props.gradingScheme,
pointsBasedGradingScheme: this.props.pointsBasedGradingScheme,
pointsPossible: this.props.assignment.pointsPossible,
})
}

View File

@ -30,6 +30,7 @@ type Props = {
}
enterGradesAs: 'points' | 'percent' | 'passFail' | 'gradingScheme'
gradingScheme: Array<Array<string | number>>
pointsBasedGradingScheme: boolean
submission: SubmissionData
}
@ -40,6 +41,7 @@ export default function LatePolicyGrade(props: Props) {
formatType: props.enterGradesAs,
pointsPossible: props.assignment.pointsPossible,
gradingScheme: props.gradingScheme,
pointsBasedGradingScheme: props.pointsBasedGradingScheme,
version: 'final',
}
const finalGrade = GradeFormatHelper.formatSubmissionGrade(props.submission, formatOptions)

View File

@ -116,6 +116,7 @@ export type SubmissionTrayProps = {
onClose: () => void
requireStudentGroupForSpeedGrader: boolean
gradingScheme: null | GradingStandard[]
pointsBasedGradingScheme: boolean
onGradeSubmission: (submission: CamelizedSubmission, gradeInfo: GradeResult) => void
onRequestClose: () => void
selectNextAssignment: () => void
@ -475,6 +476,7 @@ export default class SubmissionTray extends React.Component<
disabled={this.props.gradingDisabled}
enterGradesAs={this.props.enterGradesAs}
gradingScheme={this.props.gradingScheme}
pointsBasedGradingScheme={this.props.pointsBasedGradingScheme}
pendingGradeInfo={this.props.pendingGradeInfo}
onSubmissionUpdate={this.props.onGradeSubmission}
submission={this.props.submission}
@ -486,6 +488,7 @@ export default class SubmissionTray extends React.Component<
assignment={this.props.assignment}
enterGradesAs={this.props.enterGradesAs}
gradingScheme={this.props.gradingScheme}
pointsBasedGradingScheme={this.props.pointsBasedGradingScheme}
submission={this.props.submission}
/>
</View>

View File

@ -63,6 +63,7 @@ ready(() => {
isMasterCourse={ENV.BLUEPRINT_COURSES_DATA?.isMasterCourse}
showImmersiveReader={ENV.SHOW_IMMERSIVE_READER}
gradingScheme={ENV.GRADING_SCHEME}
pointsBasedGradingScheme={ENV.POINTS_BASED}
restrictQuantitativeData={ENV.RESTRICT_QUANTITATIVE_DATA}
/>,
courseContainer

View File

@ -54,6 +54,7 @@ const GradeDetails = ({
userIsCourseAdmin,
observedUserId,
gradingScheme,
pointsBasedGradingScheme,
restrictQuantitativeData,
}) => {
const [loadingTotalGrade, setLoadingTotalGrade] = useState(true)
@ -74,7 +75,8 @@ const GradeDetails = ({
selectedGradingPeriodId,
observedUserId,
restrictQuantitativeData,
gradingScheme
gradingScheme,
pointsBasedGradingScheme
)
const grades = getAssignmentGrades(assignmentGroups, observedUserId)
const totalGrade = getTotalGradeStringFromEnrollments(
@ -82,7 +84,8 @@ const GradeDetails = ({
currentUser.id,
observedUserId,
restrictQuantitativeData,
gradingScheme
gradingScheme,
pointsBasedGradingScheme
)
const include = ['assignments', 'submission', 'read_state', 'submission_comments']
if (selectedGradingPeriodId) {
@ -267,6 +270,7 @@ GradeDetails.propTypes = {
userIsCourseAdmin: PropTypes.bool.isRequired,
observedUserId: PropTypes.string,
gradingScheme: PropTypes.array,
pointsBasedGradingScheme: PropTypes.bool,
restrictQuantitativeData: PropTypes.bool,
}

View File

@ -45,6 +45,7 @@ export const GradesPage = ({
outcomeProficiency,
observedUserId,
gradingScheme,
pointsBasedGradingScheme,
restrictQuantitativeData,
}) => {
const [loadingGradingPeriods, setLoadingGradingPeriods] = useState(true)
@ -119,6 +120,7 @@ export const GradesPage = ({
userIsCourseAdmin={userIsCourseAdmin}
observedUserId={observedUserId}
gradingScheme={gradingScheme}
pointsBasedGradingScheme={pointsBasedGradingScheme}
restrictQuantitativeData={restrictQuantitativeData}
/>
</>
@ -188,6 +190,7 @@ GradesPage.propTypes = {
outcomeProficiency: outcomeProficiencyShape,
observedUserId: PropTypes.string,
gradingScheme: PropTypes.array,
pointsBasedGradingScheme: PropTypes.bool,
restrictQuantitativeData: PropTypes.bool,
}

View File

@ -451,6 +451,7 @@ export function K5Course({
isMasterCourse,
showImmersiveReader,
gradingScheme,
pointsBasedGradingScheme,
restrictQuantitativeData,
}) {
const initialObservedId = observedUsersList.find(o => o.id === savedObservedId(currentUser.id))
@ -667,6 +668,7 @@ export function K5Course({
outcomeProficiency={outcomeProficiency}
observedUserId={showObserverOptions ? observedUserId : null}
gradingScheme={gradingScheme}
pointsBasedGradingScheme={pointsBasedGradingScheme}
restrictQuantitativeData={restrictQuantitativeData}
/>
)}
@ -732,6 +734,7 @@ K5Course.propTypes = {
isMasterCourse: PropTypes.bool.isRequired,
showImmersiveReader: PropTypes.bool.isRequired,
gradingScheme: PropTypes.array,
pointsBasedGradingScheme: PropTypes.bool,
restrictQuantitativeData: PropTypes.bool,
}

View File

@ -54,6 +54,7 @@ const GradeSummaryShape = {
showTotalsForAllGradingPeriods: PropTypes.bool,
showingAllGradingPeriods: PropTypes.bool,
gradingScheme: PropTypes.array,
pointsBasedGradingScheme: PropTypes.bool,
restrictQuantitativeData: PropTypes.bool,
}
@ -99,12 +100,13 @@ export const GradeSummaryLine = ({
showTotalsForAllGradingPeriods,
showingAllGradingPeriods,
gradingScheme,
pointsBasedGradingScheme,
restrictQuantitativeData,
}) => {
let gradeText = grade
let isPercentage = false
if (restrictQuantitativeData) {
gradeText = scoreToGrade(score, gradingScheme)
gradeText = scoreToGrade(score, gradingScheme, pointsBasedGradingScheme)
} else if (!grade) {
if (score || score === 0) {
gradeText = I18n.toPercentage(score, {

View File

@ -50,7 +50,7 @@ $(document).ready(function () {
gradeToShow = '--'
} else if (totals.grade || totals.grade === 0) {
gradeToShow = totals.restrict_quantitative_data
? scoreToGrade(totals.grade, totals.grading_scheme)
? scoreToGrade(totals.grade, totals.grading_scheme, totals.points_based_grading_scheme)
: totals.grade + '%'
} else {
gradeToShow = I18n.t('no grade')

View File

@ -62,7 +62,7 @@ export function FinalGradeOverrideTextBox({
setFinalGradeOverridePercentage('')
setInputValue('')
} else if (gradingScheme && gradingScheme.data.length > 0) {
const grade = scoreToGrade(percentage, gradingScheme.data)
const grade = scoreToGrade(percentage, gradingScheme.data, gradingScheme.pointsBased)
const inputVal = GradeFormatHelper.replaceDashWithMinus(grade)
setInputValue(inputVal || '')
if (!gradingScheme.pointsBased) {

View File

@ -39,6 +39,7 @@ export interface EnvAssignmentsA2StudentView {
originality_reports_for_a2_enabled: boolean
restrict_quantitative_data: boolean
grading_scheme: any
points_based: boolean
// Peer review data
peer_review_available: boolean

View File

@ -31,7 +31,7 @@ import type {GradeType, DeprecatedGradingScheme, GradeEntryMode} from '../gradin
function schemeKeyForPercentage(percentage, gradingScheme: DeprecatedGradingScheme) {
if (gradingScheme) {
const grade = scoreToGrade(percentage, gradingScheme.data)
const grade = scoreToGrade(percentage, gradingScheme.data, gradingScheme.pointsBased)
return GradeFormatHelper.replaceDashWithMinus(grade)
}
return null

View File

@ -131,14 +131,14 @@ function formatPercentageGrade(score, options) {
function formatGradingSchemeGrade(score, grade, options = {}) {
let formattedGrade
if (options?.restrict_quantitative_data && options.pointsPossible === 0 && score >= 0) {
formattedGrade = scoreToGrade(100, options.gradingScheme)
formattedGrade = scoreToGrade(100, options.gradingScheme, options.pointsBasedGradingScheme)
} else if (options.pointsPossible) {
const percent = scoreToPercentage(score, options.pointsPossible)
formattedGrade = scoreToGrade(percent, options.gradingScheme)
formattedGrade = scoreToGrade(percent, options.gradingScheme, options.pointsBasedGradingScheme)
} else if (grade != null) {
formattedGrade = grade
} else {
formattedGrade = scoreToGrade(score, options.gradingScheme)
formattedGrade = scoreToGrade(score, options.gradingScheme, options.pointsBasedGradingScheme)
}
return replaceDashWithMinus(formattedGrade)
@ -219,6 +219,7 @@ const GradeFormatHelper = {
// at this stage, gradingType is either points or percent, or the passed grade is a number
formattedGrade = formatGradingSchemeGrade(options.score, null, {
gradingScheme: options.grading_scheme,
pointsBasedGradingScheme: options.points_based_grading_scheme,
pointsPossible: options.pointsPossible,
restrict_quantitative_data: options.restrict_quantitative_data,
})
@ -242,6 +243,7 @@ const GradeFormatHelper = {
) {
formattedGrade = formatGradingSchemeGrade(options.score, null, {
gradingScheme: options.grading_scheme,
pointsBasedGradingScheme: options.points_based_grading_scheme,
pointsPossible: options.pointsPossible,
restrict_quantitative_data: options.restrict_quantitative_data,
})
@ -255,6 +257,7 @@ const GradeFormatHelper = {
) {
formattedGrade = formatGradingSchemeGrade(options.score, null, {
gradingScheme: options.grading_scheme,
pointsBasedGradingScheme: options.points_based_grading_scheme,
pointsPossible: options.pointsPossible,
restrict_quantitative_data: options.restrict_quantitative_data,
})

View File

@ -74,7 +74,7 @@ function parseAsGradingScheme(value: number, options): null | GradeInput {
enteredAs: 'gradingScheme',
percent: options.pointsPossible ? percentage : 0,
points: options.pointsPossible ? pointsFromPercentage(percentage, options.pointsPossible) : 0,
schemeKey: scoreToGrade(percentage, options.gradingScheme),
schemeKey: scoreToGrade(percentage, options.gradingScheme, options.pointsBasedGradingScheme),
}
}
@ -99,7 +99,7 @@ function parseAsPercent(value: string, options): null | GradeInput {
enteredAs: 'percent',
percent,
points,
schemeKey: scoreToGrade(percent, options.gradingScheme),
schemeKey: scoreToGrade(percent, options.gradingScheme, options.pointsBasedGradingScheme),
}
}
@ -115,7 +115,7 @@ function parseAsPoints(value: string, options): null | GradeInput {
enteredAs: 'points',
percent: null,
points,
schemeKey: scoreToGrade(percent, options.gradingScheme),
schemeKey: scoreToGrade(percent, options.gradingScheme, options.pointsBasedGradingScheme),
}
}

View File

@ -341,6 +341,7 @@ export type FormatGradeOptions = {
score?: number | null
restrict_quantitative_data?: boolean
grading_scheme?: DeprecatedGradingScheme[]
points_based_grading_scheme?: boolean
}
/**

View File

@ -52,6 +52,7 @@ export const transformGrades = courses =>
isHomeroom: course.homeroom_course,
enrollments: course.enrollments,
gradingScheme: course.grading_scheme,
pointsBasedGradingScheme: course.points_based_grading_scheme,
restrictQuantitativeData: course.restrict_quantitative_data,
}
return getCourseGrades(basicCourseInfo)
@ -173,7 +174,8 @@ export const getAssignmentGroupTotals = (
gradingPeriodId,
observedUserId,
restrictQuantitativeData = false,
gradingScheme = []
gradingScheme = [],
pointsBasedGradingScheme = false
) => {
if (gradingPeriodId) {
data = data.filter(group =>
@ -207,7 +209,7 @@ export const getAssignmentGroupTotals = (
} else {
const tempScore = (groupScores.current.score / groupScores.current.possible) * 100
score = restrictQuantitativeData
? scoreToGrade(tempScore, gradingScheme)
? scoreToGrade(tempScore, gradingScheme, pointsBasedGradingScheme)
: I18n.n(tempScore, {percentage: true, precision: 2})
}
@ -232,6 +234,7 @@ const formatGradeToRQD = (assignment, submission) => {
score: submission?.score,
restrict_quantitative_data: ENV.RESTRICT_QUANTITATIVE_DATA,
grading_scheme: ENV.GRADING_SCHEME,
points_based_grading_scheme: ENV.POINTS_BASED,
})
}
@ -285,7 +288,8 @@ export const getTotalGradeStringFromEnrollments = (
userId,
observedUserId,
restrictQuantitativeData = false,
gradingScheme = []
gradingScheme = [],
pointsBasedGradingScheme
) => {
let grades
if (observedUserId) {
@ -300,7 +304,7 @@ export const getTotalGradeStringFromEnrollments = (
return I18n.t('n/a')
}
if (restrictQuantitativeData) {
return scoreToGrade(grades.current_score, gradingScheme)
return scoreToGrade(grades.current_score, gradingScheme, pointsBasedGradingScheme)
}
const score = I18n.n(grades.current_score, {percentage: true, precision: 2})
return grades.current_grade == null