Consider "Enter Grades as" setting with grading schemes

Do not match grading scheme values if the Gradebook setting for
"Enter Grades as" is set to "Points". See the community post in
the ticket to get a better understanding of the issue.

closes EVAL-2117
flag=none

Test Plan:
As a teacher:
1. Select/create a grading scheme that includes numbers e.g.
Name                         Range
5                            100% to 90.0%
4                            < 90.0% to 70.0%
3                            < 70.0% to 50.0%
Revision
required/Komplettering       < 50.0% to 25.0%
U                            < 25.0% to 0.0%
2. Create an assignment worth 100 points and set "Display grade as"
to Letter grade
3. In gradebook, grade the assignment for students using names in
grading scheme and other values  and see that totals correspond to
those values e.g. (5 -> 100%), (4 -> 89%) and (6 -> 6%)
4. Switch "Enter Grades as" option to "Points". Award 5 points to
a student and see that it does not assign the value from grading
scheme but the actual percentage. Try different values matching and
not matching grading scheme names.

Change-Id: I1312c6bfb5070c0b08e7590232e86d609e446025
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/283437
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Dustin Cowles <dustin.cowles@instructure.com>
Reviewed-by: Spencer Olson <solson@instructure.com>
QA-Review: Eduardo Escobar <eduardo.escobar@instructure.com>
Product-Review: Jody Sailor
This commit is contained in:
Syed Hussain 2022-01-18 12:41:57 -06:00
parent bfc1931b09
commit bce89a6354
7 changed files with 171 additions and 20 deletions

View File

@ -725,6 +725,9 @@ class SubmissionsApiController < ApplicationController
# @argument include[visibility] [String]
# Whether this assignment is visible to the owner of the submission
#
# @argument prefer_points_over_scheme [Boolean]
# Treat posted_grade as points if the value matches a grading scheme value
#
# @argument submission[posted_grade] [String]
# Assign a score to the submission, updating both the "score" and "grade"
# fields on the submission record. This parameter can be passed in a few
@ -841,6 +844,7 @@ class SubmissionsApiController < ApplicationController
submission[:submission_type] = params[:submission][:submission_type]
submission[:url] = params[:submission][:url]
end
submission[:prefer_points_over_scheme] = value_to_boolean(params[:prefer_points_over_scheme])
end
if submission[:grade] || submission[:excuse]

View File

@ -1455,14 +1455,14 @@ class Assignment < ActiveRecord::Base
round_if_whole(result).to_s
end
def interpret_grade(grade)
def interpret_grade(grade, prefer_points_over_scheme: false)
case grade.to_s
when /^[+-]?\d*\.?\d+%$/
# interpret as a percentage
percentage = grade.to_f / 100.0.to_d
points_possible.to_f * percentage
when /^[+-]?\d*\.?\d+$/
if uses_grading_standard && (standard_based_score = grading_standard_or_default.grade_to_score(grade))
if !prefer_points_over_scheme && uses_grading_standard && (standard_based_score = grading_standard_or_default.grade_to_score(grade))
(points_possible || 0.0) * standard_based_score / 100.0
else
grade.to_f
@ -1481,10 +1481,10 @@ class Assignment < ActiveRecord::Base
end
end
def grade_to_score(grade = nil)
def grade_to_score(grade = nil, prefer_points_over_scheme: false)
return nil if grade.blank?
parsed_grade = interpret_grade(grade)
parsed_grade = interpret_grade(grade, prefer_points_over_scheme: prefer_points_over_scheme)
case self.grading_type
when "points", "percent", "letter_grade", "gpa_scale"
score = parsed_grade
@ -1900,11 +1900,11 @@ class Assignment < ActiveRecord::Base
all_submissions.where(user_id: user_id).first_or_initialize
end
def compute_grade_and_score(grade, score)
def compute_grade_and_score(grade, score, prefer_points_over_scheme: false)
grade = nil if grade == ""
if grade
score = grade_to_score(grade)
score = grade_to_score(grade, prefer_points_over_scheme: prefer_points_over_scheme)
end
if score
grade = score_to_grade(score, grade)
@ -2021,7 +2021,7 @@ class Assignment < ActiveRecord::Base
return if submission.user != original_student && submission.excused?
grader = opts[:grader]
grade, score = compute_grade_and_score(opts[:grade], opts[:score])
grade, score = compute_grade_and_score(opts[:grade], opts[:score], prefer_points_over_scheme: opts[:prefer_points_over_scheme])
did_grade = false
submission.attributes = opts.slice(:submission_type, :url, :body)

View File

@ -3197,6 +3197,84 @@ describe "Submissions API", type: :request do
}.by(1)
end
context "grading scheme with numerics in names" do
before do
@standard = @course.grading_standards.create!(title: "course standard", standard_data: { a: { name: "5", value: "90" }, b: { name: "4", value: "70" }, c: { name: "3", value: "50" }, d: { name: "Revision required/Komplettering", value: "25" }, e: { name: "U", value: "0" } })
@assignment.update_attribute :grading_standard, @standard
api_call(:put, "/api/v1/courses/#{@course.id}",
{ controller: "courses", action: "update", format: "json", id: @course.to_param }, course: { grading_standard_id: @standard.id })
@course.reload
end
it "can grade when it matches grading name and enter grades is set to points" do
json = api_call(
:put,
"/api/v1/courses/#{@course.id}/assignments/#{@assignment.id}/submissions/#{@student.id}.json",
{
controller: "submissions_api",
action: "update",
format: "json",
course_id: @course.id.to_s,
assignment_id: @assignment.id.to_s,
user_id: @student.id.to_s
}, {
submission: {
posted_grade: 5
},
prefer_points_over_scheme: true
}
)
expect(json["grade"]).to eq "Revision required/Komplettering"
expect(json["score"]).to eq 5.0
end
it "can grade when it matches grading scheme name" do
json = api_call(
:put,
"/api/v1/courses/#{@course.id}/assignments/#{@assignment.id}/submissions/#{@student.id}.json",
{
controller: "submissions_api",
action: "update",
format: "json",
course_id: @course.id.to_s,
assignment_id: @assignment.id.to_s,
user_id: @student.id.to_s
}, {
submission: {
posted_grade: 5
}
}
)
expect(json["grade"]).to eq "5"
expect(json["score"]).to eq @assignment.points_possible
end
it "can grade when it does not match grading scheme name" do
# grading_type = "letter_grade"
json = api_call(
:put,
"/api/v1/courses/#{@course.id}/assignments/#{@assignment.id}/submissions/#{@student.id}.json",
{
controller: "submissions_api",
action: "update",
format: "json",
course_id: @course.id.to_s,
assignment_id: @assignment.id.to_s,
user_id: @student.id.to_s
}, {
submission: {
posted_grade: 9
}
}
)
expect(json["grade"]).to eq "3"
expect(json["score"]).to eq 9.0
end
end
context "group assignments" do
before do
@student2 = @course.enroll_student(User.create!, enrollment_state: :active).user

View File

@ -50,7 +50,7 @@ QUnit.module('GradebookApi.createTeacherNotesColumn', {
}
})
test('sends a post request to the "create teacher notes column" url', function() {
test('sends a post request to the "create teacher notes column" url', function () {
return GradebookApi.createTeacherNotesColumn('1201').then(() => {
const request = this.getRequest()
equal(request.method, 'POST')
@ -58,7 +58,7 @@ test('sends a post request to the "create teacher notes column" url', function()
})
})
test('includes data to create a teacher notes column', function() {
test('includes data to create a teacher notes column', function () {
return GradebookApi.createTeacherNotesColumn('1201').then(() => {
const bodyData = JSON.parse(this.getRequest().requestBody)
equal(bodyData.column.title, 'Notes')
@ -67,7 +67,7 @@ test('includes data to create a teacher notes column', function() {
})
})
test('includes required request headers', function() {
test('includes required request headers', function () {
return GradebookApi.createTeacherNotesColumn('1201').then(() => {
const {requestHeaders} = this.getRequest()
ok(
@ -82,7 +82,7 @@ test('includes required request headers', function() {
})
})
test('sends the column data to the success handler', function() {
test('sends the column data to the success handler', function () {
return GradebookApi.createTeacherNotesColumn('1201').then(({data}) => {
deepEqual(data, this.customColumn)
})
@ -113,7 +113,7 @@ QUnit.module('GradebookApi.updateTeacherNotesColumn', {
}
})
test('sends a post request to the "create teacher notes column" url', function() {
test('sends a post request to the "create teacher notes column" url', function () {
return GradebookApi.updateTeacherNotesColumn('1201', '2401', {hidden: true}).then(() => {
const request = this.getRequest()
equal(request.method, 'PUT')
@ -121,14 +121,14 @@ test('sends a post request to the "create teacher notes column" url', function()
})
})
test('includes params for updating a teacher notes column', function() {
test('includes params for updating a teacher notes column', function () {
return GradebookApi.updateTeacherNotesColumn('1201', '2401', {hidden: true}).then(() => {
const bodyData = JSON.parse(this.getRequest().requestBody)
equal(bodyData.column.hidden, true)
})
})
test('includes required request headers', function() {
test('includes required request headers', function () {
return GradebookApi.updateTeacherNotesColumn('1201', '2401', {hidden: true}).then(() => {
const {requestHeaders} = this.getRequest()
ok(
@ -143,7 +143,7 @@ test('includes required request headers', function() {
})
})
test('sends the column data to the success handler', function() {
test('sends the column data to the success handler', function () {
return GradebookApi.updateTeacherNotesColumn('1201', '2401', {hidden: true}).then(({data}) => {
deepEqual(data, this.customColumn)
})
@ -207,4 +207,32 @@ QUnit.module('GradebookApi.updateSubmission', hooks => {
}).then(({data}) => {
deepEqual(data, submissionData)
}))
test('sends true for prefer_points_over_scheme param when passed "points"', () =>
GradebookApi.updateSubmission(
courseId,
assignmentId,
userId,
{
latePolicyStatus: 'none'
},
'points'
).then(() => {
const bodyData = JSON.parse(getRequest().requestBody)
strictEqual(bodyData.prefer_points_over_scheme, true)
}))
test('sends false for prefer_points_over_scheme param when not passed "points"', () =>
GradebookApi.updateSubmission(
courseId,
assignmentId,
userId,
{
latePolicyStatus: 'none'
},
'percent'
).then(() => {
const bodyData = JSON.parse(getRequest().requestBody)
strictEqual(bodyData.prefer_points_over_scheme, false)
}))
})

View File

@ -3421,6 +3421,33 @@ describe Assignment do
expect(decimal_part.length).to be <= 3
end
context "with numeric grading standard" do
before(:once) do
@assignment.update!(grading_type: "letter_grade", points_possible: 10.0)
grading_standard = @course.grading_standards.build(title: "Number Before Letter")
grading_standard.data = {
"1" => 0.9,
"2" => 0.8,
"3" => 0.7,
"4" => 0.6,
"5" => 0.5,
"6" => 0
}
grading_standard.assignments << @assignment
grading_standard.save!
end
it "does not match a numeric grading standard if points are preferred over grading scheme value" do
@assignment.points_possible = 100
expect(@assignment.interpret_grade("1", prefer_points_over_scheme: true)).to eq 1.0
end
it "matches a numeric grading standard if grading scheme value is preferred over points" do
@assignment.points_possible = 100
expect(@assignment.interpret_grade("1")).to eq 100.0
end
end
context "with alphanumeric grades" do
before(:once) do
@assignment.update!(grading_type: "letter_grade", points_possible: 10.0)

View File

@ -4447,14 +4447,20 @@ class Gradebook extends React.Component<GradebookProps, GradebookState> {
})
}
apiUpdateSubmission = (submission, gradeInfo) => {
apiUpdateSubmission(submission, gradeInfo, enterGradesAs) {
const {userId, assignmentId} = submission
const student = this.student(userId)
this.addPendingGradeInfo(submission, gradeInfo)
if (this.getSubmissionTrayState().open) {
this.renderSubmissionTray(student)
}
return GradebookApi.updateSubmission(this.options.context_id, assignmentId, userId, submission)
return GradebookApi.updateSubmission(
this.options.context_id,
assignmentId,
userId,
submission,
enterGradesAs
)
.then(response => {
this.removePendingGradeInfo(submission)
this.updateSubmissionsFromExternal(response.data.all_submissions)
@ -4497,7 +4503,11 @@ class Gradebook extends React.Component<GradebookProps, GradebookState> {
} else {
submissionData.posted_grade = gradeInfo.score
}
return this.apiUpdateSubmission(submissionData, gradeInfo).then(response => {
return this.apiUpdateSubmission(
submissionData,
gradeInfo,
gradeChangeOptions.enterGradesAs
).then(response => {
const assignment = this.getAssignment(submission.assignmentId)
const outlierScoreHelper = new OutlierScoreHelper(
response.data.score,

View File

@ -42,9 +42,13 @@ function updateTeacherNotesColumn(courseId, columnId, attr) {
return axios.put(url, {column: attr})
}
function updateSubmission(courseId, assignmentId, userId, submission) {
function updateSubmission(courseId, assignmentId, userId, submission, enterGradesAs) {
const url = `/api/v1/courses/${courseId}/assignments/${assignmentId}/submissions/${userId}`
return axios.put(url, {submission: underscore(submission), include: ['visibility']})
return axios.put(url, {
submission: underscore(submission),
include: ['visibility'],
prefer_points_over_scheme: enterGradesAs === 'points'
})
}
function saveUserSettings(courseId, gradebook_settings) {