bring ab_guid to assignments

closes EVAL-3393
closes EVAL-3394
closes EVAL-3395

flag=none

[fsc-timeout=100]

test plan:
- all tests pass
- create an assignment with an ab_guid
- using the assignments api, verify that the ab_guid is returned in the
    show response when `?include[]=ab_guid is passed`
- using the assignments api, verify that the ab_guid is returned in the
    index response when `?include[]=ab_guid is passed`
- using the assignments api, verify that you are able to update the
    ab_guid

Change-Id: Ic4e8d78a6b4dfb112168ec68bd7f6e117a8030f5
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/324884
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Spencer Olson <solson@instructure.com>
Reviewed-by: Kai Bjorkman <kbjorkman@instructure.com>
QA-Review: Cameron Ray <cameron.ray@instructure.com>
Product-Review: Cameron Ray <cameron.ray@instructure.com>
Migration-Review: Jacob Burroughs <jburroughs@instructure.com>
This commit is contained in:
Derek Williams 2023-08-10 08:45:47 -04:00
parent e748efc22e
commit 4f3317ccd8
5 changed files with 201 additions and 6 deletions

View File

@ -661,6 +661,12 @@
# "example": true,
# "type": "boolean"
# },
# "ab_guid": {
# "description": "(Optional) The academic benchmark(s) associated with the assignment or the assignment's rubric. Only included if 'ab_guid' is included in the 'include' parameter.",
# "example": ["ABCD","EFGH"],
# "type": "array",
# "items": {"type": "string"}
# },
# "annotatable_attachment_id": {
# "description": "The id of the attachment to be annotated by students. Relevant only if submission_types includes 'student_annotation'.",
# "type": "integer"
@ -758,7 +764,7 @@ class AssignmentsApiController < ApplicationController
# @API List assignments
# Returns the paginated list of assignments for the current course or assignment group.
# @argument include[] [String, "submission"|"assignment_visibility"|"all_dates"|"overrides"|"observed_users"|"can_edit"|"score_statistics"]
# @argument include[] [String, "submission"|"assignment_visibility"|"all_dates"|"overrides"|"observed_users"|"can_edit"|"score_statistics"|"ab_guid"]
# Optional information to include with each assignment:
# submission:: The current user's current +Submission+
# assignment_visibility:: An array of ids of students who can see the assignment
@ -767,6 +773,7 @@ class AssignmentsApiController < ApplicationController
# observed_users:: An array of submissions for observed users
# can_edit:: an extra Boolean value will be included with each +Assignment+ (and +AssignmentDate+ if +all_dates+ is supplied) to indicate whether the caller can edit the assignment or date. Moderated grading and closed grading periods may restrict a user's ability to edit an assignment.
# score_statistics:: An object containing min, max, and mean score on this assignment. This will not be included for students if there are less than 5 graded assignments or if disabled by the instructor. Only valid if 'submission' is also included.
# ab_guid:: An array of guid strings for academic benchmarks
# @argument search_term [String]
# The partial title of the assignments to match and return.
# @argument override_assignment_dates [Boolean]
@ -1021,6 +1028,10 @@ class AssignmentsApiController < ApplicationController
ActiveRecord::Associations.preload(assignments, :score_statistic)
end
if include_params.include?("ab_guid")
ActiveRecord::Associations.preload(assignments, rubric: { learning_outcome_alignments: :learning_outcome })
end
mc_status = setup_master_course_restrictions(assignments, context)
assignments.map do |assignment|
@ -1047,6 +1058,7 @@ class AssignmentsApiController < ApplicationController
preloaded_user_content_attachments: preloaded_attachments,
include_can_edit: include_params.include?("can_edit"),
include_score_statistics: include_params.include?("score_statistics"),
include_ab_guid: include_params.include?("ab_guid"),
master_course_status: mc_status)
end
end
@ -1054,7 +1066,7 @@ class AssignmentsApiController < ApplicationController
# @API Get a single assignment
# Returns the assignment with the given id.
# @argument include[] [String, "submission"|"assignment_visibility"|"overrides"|"observed_users"|"can_edit"|"score_statistics"]
# @argument include[] [String, "submission"|"assignment_visibility"|"overrides"|"observed_users"|"can_edit"|"score_statistics"|"ab_guid"]
# Associations to include with the assignment. The "assignment_visibility" option
# requires that the Differentiated Assignments course feature be turned on. If
# "observed_users" is passed, submissions for observed users will also be included.
@ -1098,7 +1110,8 @@ class AssignmentsApiController < ApplicationController
include_can_edit: included_params.include?("can_edit"),
include_score_statistics: included_params.include?("score_statistics"),
include_can_submit: included_params.include?("can_submit"),
include_webhook_info: included_params.include?("webhook_info")
include_webhook_info: included_params.include?("webhook_info"),
include_ab_guid: included_params.include?("ab_guid")
}
result_json = if use_quiz_json?

View File

@ -770,6 +770,11 @@ class Assignment < ActiveRecord::Base
true
end
def ab_guid_through_rubric
# ab_guid is an academic benchmark guid - it can be saved on the assignmenmt itself, or accessed through this association
rubric&.learning_outcome_alignments&.map { |loa| loa.learning_outcome.vendor_guid }&.compact || []
end
def update_student_submissions(updating_user)
graded_at = Time.zone.now
submissions.graded.preload(:user).find_each do |s|

View File

@ -0,0 +1,26 @@
# frozen_string_literal: true
#
# Copyright (C) 2023 - present 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/>.
class AddAbGuidToAssignments < ActiveRecord::Migration[7.0]
tag :predeploy
def change
add_column :assignments, :ab_guid, :string, array: true, default: [], null: false, if_not_exists: true
end
end

View File

@ -458,6 +458,10 @@ module Api::V1::Assignment
(submission.nil? || submission.attempts_left.nil? || submission.attempts_left > 0)
end
if opts[:include_ab_guid]
hash["ab_guid"] = assignment.ab_guid.presence || assignment.ab_guid_through_rubric
end
hash["restrict_quantitative_data"] = assignment.restrict_quantitative_data?(user, true) || false
hash
@ -734,6 +738,14 @@ module Api::V1::Assignment
assignment.assignment_group = assignment.context.assignment_groups.where(id: ag_id).first
end
if update_params.key?("ab_guid")
assignment.ab_guid.clear
ab_guids = update_params.delete("ab_guid").presence
Array(ab_guids).each do |guid|
assignment.ab_guid << guid if guid.present?
end
end
if update_params.key?("group_category_id") && !assignment.group_category_deleted_with_submissions?
gc_id = update_params.delete("group_category_id").presence
assignment.group_category = assignment.context.group_categories.where(id: gc_id).first
@ -1151,7 +1163,8 @@ module Api::V1::Assignment
{ "allowed_extensions" => strong_anything },
{ "integration_data" => strong_anything },
{ "external_tool_tag_attributes" => strong_anything },
({ "submission_types" => strong_anything } if should_update_submission_types)
({ "submission_types" => strong_anything } if should_update_submission_types),
{ "ab_guid" => strong_anything },
].compact
end

View File

@ -29,6 +29,8 @@ describe AssignmentsApiController, type: :request do
include Api::V1::Submission
include LtiSpecHelper
specs_require_sharding
context "locked api item" do
let(:item_type) { "assignment" }
@ -103,6 +105,18 @@ describe AssignmentsApiController, type: :request do
expect(json.first).to have_key("in_closed_grading_period")
end
it "includes ab_guid in returned json when included[]='ab_guid' is passed" do
@course.assignments.create!(title: "Example Assignment")
json = api_get_assignments_index_from_course(@course, include: ["ab_guid"])
expect(json.first).to have_key("ab_guid")
end
it "does not include ab_guid in returned json when included[]='ab_guid' is not passed" do
@course.assignments.create!(title: "Example Assignment")
json = api_get_assignments_index_from_course(@course)
expect(json.first).not_to have_key("ab_guid")
end
it "includes due_date_required in returned json" do
@course.assignments.create!(title: "Example Assignment")
json = api_get_assignments_index_from_course(@course)
@ -285,8 +299,6 @@ describe AssignmentsApiController, type: :request do
end
describe "sharding" do
specs_require_sharding
before do
@shard1.activate do
account = Account.create!
@ -3776,6 +3788,71 @@ describe AssignmentsApiController, type: :request do
expect(@assignment.submission_types).to eq "not_graded"
end
it "leaves ab_guid alone if not included in update params" do
@assignment = @course.assignments.create!(
name: "some assignment",
points_possible: 15,
submission_types: "online_text_entry",
grading_type: "percent",
ab_guid: ["a", "b"]
)
api_update_assignment_call(@course, @assignment, title: "new title")
expect(response).to be_successful
expect(@assignment.reload.ab_guid).to eq ["a", "b"]
end
it "updates ab_guid if included in update params" do
@assignment = @course.assignments.create!(
name: "some assignment",
points_possible: 15,
submission_types: "online_text_entry",
grading_type: "percent",
ab_guid: ["a", "b"]
)
api_update_assignment_call(@course, @assignment, ab_guid: ["c", "d"])
expect(response).to be_successful
expect(@assignment.reload.ab_guid).to eq ["c", "d"]
end
it "updates ab_guid to empty array if included in update params and empty" do
@assignment = @course.assignments.create!(
name: "some assignment",
points_possible: 15,
submission_types: "online_text_entry",
grading_type: "percent",
ab_guid: ["a", "b"]
)
api_update_assignment_call(@course, @assignment, ab_guid: [])
expect(response).to be_successful
expect(@assignment.reload.ab_guid).to eq []
end
it "updates ab_guid to empty array if included in update params and is empty string" do
@assignment = @course.assignments.create!(
name: "some assignment",
points_possible: 15,
submission_types: "online_text_entry",
grading_type: "percent",
ab_guid: ["a", "b"]
)
api_update_assignment_call(@course, @assignment, ab_guid: "")
expect(response).to be_successful
expect(@assignment.reload.ab_guid).to eq []
end
it "updates ab_guid to a single element array if a string is passed in" do
@assignment = @course.assignments.create!(
name: "some assignment",
points_possible: 15,
submission_types: "online_text_entry",
grading_type: "percent",
ab_guid: ["a", "b"]
)
api_update_assignment_call(@course, @assignment, ab_guid: "c")
expect(response).to be_successful
expect(@assignment.reload.ab_guid).to eq ["c"]
end
describe "annotatable attachment" do
before(:once) do
@assignment = @course.assignments.create!(name: "Some Assignment")
@ -6428,6 +6505,67 @@ describe AssignmentsApiController, type: :request do
expect(json).not_to have_key("can_submit")
end
end
context "ab_guid" do
before do
course_with_student_logged_in(course_name: "Course 1", active_all: 1)
@course.start_at = 14.days.ago
@course.save!
@assignment = @course.assignments.create!(title: "Assignment 1",
points_possible: 10,
submission_types: "online_text_entry",
ab_guid: ["1234"])
account = Account.default
outcome = account.created_learning_outcomes.create!(
title: "My Outcome",
description: "Description of my outcome",
vendor_guid: "vendorguid9000"
)
rating = [{ id: "rat1",
description: "Full Marks",
long_description: "Student did a great job.",
points: 5.0 }]
criteria = [{ id: 1, points: 9000, learning_outcome_id: outcome.id, description: "description", long_description: "long description", ratings: rating }]
@assignment2 = @course.assignments.create!(title: "Assignment 2")
@rubric = @course.rubrics.create!(title: "My Rubric", context: @course, data: criteria)
@assignment2.rubric = @rubric
@assignment2.save!
@assignment2.rubric_association.context = @course
@assignment2.rubric_association.save!
@assignment3 = @course.assignments.create!(title: "Assignment 3")
end
def get_assignment_with_guid(assignment_id)
api_call(:get,
"/api/v1/courses/#{@course.id}/assignments/#{assignment_id}?include[]=ab_guid",
{ controller: "assignments_api",
action: "show",
format: "json",
course_id: @course.id.to_s,
id: assignment_id,
include: ["ab_guid"] })
end
it "returns ab_guid when it is included in include param" do
json = get_assignment_with_guid(@assignment.id)
expect(json).to have_key("ab_guid")
expect(json["ab_guid"]).to eq(["1234"])
end
it "returns vendor_id through rubrics if no ab_guid is present" do
json = get_assignment_with_guid(@assignment2.id)
expect(json).to have_key("ab_guid")
expect(json["ab_guid"]).to eq(["vendorguid9000"])
end
it "returns an empty array if ab_guid is requested and none exists on assignment or through rubric" do
json = get_assignment_with_guid(@assignment3.id)
expect(json).to have_key("ab_guid")
expect(json["ab_guid"]).to eq([])
end
end
end
context "update_from_params" do