expose launch url in AGS Line Item API

closes INTEROP-7267
flag=none

why:
* to facilitate 1.1 to 1.3 migration for tools that may only be able to
link line items using the original resource link url

test plan:
* install the LTI 1.3 test tool
* get an LTI token for that tool using
`canvas.docker/api/lti/advantage_token?tool_id=1`,
replacing 1 with the id of your 1.3 test tool
* construct an API request for the AGS Line Items API  for a course
with at least one line item:
`canvas.docker/api/lti/courses/1/line_items`,
and add the LTI token to the Authorization header using
`Bearer: <token>`
* the request should return a list of line items in JSON format
* add `?include[]=launch_url` to the request
* each line item should now have a new canvas extension property that
contains the launch url for that line item and its related assignment
* change the request to request a specific line item (like add `/1` on
the end), still with the query param
* it should return one line item, with the new extension property
* remove the query param or change the value to something else
* it should not return the extension property

Change-Id: I0caa494111fde4093f99fef1398b27799b07a6cf
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/287096
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Mysti Lilla <mysti@instructure.com>
QA-Review: Mysti Lilla <mysti@instructure.com>
Product-Review: Alexis Nast <alexis.nast@instructure.com>
This commit is contained in:
Xander Moffatt 2022-03-14 16:38:34 -06:00
parent 6f60f41833
commit 0e3ddfd48b
6 changed files with 117 additions and 16 deletions

View File

@ -62,6 +62,11 @@ module Lti
# "description": "The extension that defines the submission_type of the line_item. Only returns if set through the line_item create endpoint.",
# "example": "{\n\t\"type\":\"external_tool\",\n\t\"external_tool_url\":\"https://my.launch.url\",\n}",
# "type": "string"
# },
# "https://canvas.instructure.com/lti/launch_url": {
# "description": "The launch url of the Line Item. Only returned if `include=launch_url` query parameter is passed, and only for Show and List actions.",
# "example": "https://my.tool.url/launch",
# "type": "string"
# }
# }
# }
@ -175,16 +180,22 @@ module Lti
# @API Show a Line Item
# Show existing Line Item
#
# @argument include[] [String, "launch_url"]
# Array of additional information to include.
#
# "launch_url":: includes the launch URL for this line item using the "https\://canvas.instructure.com/lti/launch_url" extension
#
# @returns LineItem
def show
# the LineItem workflow_state still "active" even if the assignment is deleted
head :not_found and return if line_item.assignment.deleted?
render json: LineItemsSerializer.new(line_item, line_item_id(line_item)),
render json: LineItemsSerializer.new(line_item, line_item_id(line_item), include_launch_url?),
content_type: MIME_TYPE
end
# @API List line Items
# List all Line Items for a course
#
# @argument tag [String]
# If specified only Line Items with this tag will be included.
@ -198,6 +209,11 @@ module Lti
# @argument limit [String]
# May be used to limit the number of Line Items returned in a page
#
# @argument include[] [String, "launch_url"]
# Array of additional information to include.
#
# "launch_url":: includes the launch URL for each line item using the "https\://canvas.instructure.com/lti/launch_url" extension
#
# @returns LineItem
def index
line_items = Api.paginate(
@ -223,6 +239,10 @@ module Lti
private
def include_launch_url?
params[:include]&.include? "launch_url"
end
def line_item_params
@_line_item_params ||= params.permit(%i[resourceId resourceLinkId scoreMaximum label tag],
Lti::LineItem::AGS_EXT_SUBMISSION_TYPE => [:type, :external_tool_url]).transform_keys do |k|
@ -278,7 +298,7 @@ module Lti
end
def line_item_collection(line_items)
line_items.map { |li| LineItemsSerializer.new(li, line_item_id(li)) }
line_items.map { |li| LineItemsSerializer.new(li, line_item_id(li), include_launch_url?) }
end
def verify_valid_resource_link

View File

@ -47,12 +47,18 @@ class Lti::LineItem < ApplicationRecord
before_destroy :destroy_resource_link, if: :assignment_line_item? # assignment will destroy all the other line_items of a resourceLink
before_destroy :destroy_assignment
AGS_EXT_SUBMISSION_TYPE = "https://canvas.instructure.com/lti/submission_type"
AGS_EXT_PREFIX = "https://canvas.instructure.com/lti/"
AGS_EXT_SUBMISSION_TYPE = "#{AGS_EXT_PREFIX}submission_type"
AGS_EXT_LAUNCH_URL = "#{AGS_EXT_PREFIX}launch_url"
def assignment_line_item?
assignment.line_items.order(:created_at).first.id == id
end
def launch_url_extension
{ AGS_EXT_LAUNCH_URL => assignment.external_tool_tag&.url }
end
def self.create_line_item!(assignment, context, tool, params)
transaction do
assignment_attr = {

View File

@ -19,9 +19,10 @@
module Lti::IMS
class LineItemsSerializer
def initialize(line_item, line_item_url)
def initialize(line_item, line_item_url, include_launch_url = false)
@line_item = line_item
@line_item_url = line_item_url
@include_launch_url = include_launch_url
end
def as_json
@ -31,8 +32,10 @@ module Lti::IMS
label: @line_item.label,
resourceId: @line_item.resource_id,
tag: @line_item.tag,
resourceLinkId: @line_item.resource_link&.resource_link_uuid
}.merge(@line_item.extensions).compact
resourceLinkId: @line_item.resource_link&.resource_link_uuid,
}.merge(@line_item.extensions)
.merge(@include_launch_url ? @line_item.launch_url_extension : {})
.compact
end
end
end

View File

@ -575,6 +575,22 @@ module Lti
expect(response).to be_not_found
end
end
context "without include=launch_url parameter" do
it "does not include launch url extension" do
send_request
expect(parsed_response_body).not_to have_key(Lti::LineItem::AGS_EXT_LAUNCH_URL)
end
end
context "with include[]=launch_url parameter" do
let(:params_overrides) { super().merge({ "include[]" => "launch_url" }) }
it "includes launch url extension in line item json" do
send_request
expect(parsed_response_body).to include(Lti::LineItem::AGS_EXT_LAUNCH_URL => tool.url)
end
end
end
describe "#index" do
@ -707,6 +723,22 @@ module Lti
send_request
expect(response.headers).to have_key("Link")
end
context "without include=launch_url parameter" do
it "does not include launch url extension" do
send_request
expect(parsed_response_body).not_to include(have_key(Lti::LineItem::AGS_EXT_LAUNCH_URL))
end
end
context "with include[]=launch_url parameter" do
let(:params_overrides) { super().merge({ "include[]" => "launch_url" }) }
it "includes launch url extension in line item json" do
send_request
expect(parsed_response_body).to all(include(Lti::LineItem::AGS_EXT_LAUNCH_URL => tool.url))
end
end
end
describe "destroy" do

View File

@ -74,6 +74,25 @@ RSpec.describe Lti::LineItem, type: :model do
end
end
describe "#launch_url_extension" do
let(:url) { "https://example.com/launch" }
let(:assignment) do
a = assignment_model
a.external_tool_tag = ContentTag.create!(context: a, url: url)
a.save!
a
end
let(:line_item) { line_item_model(assignment: assignment) }
it "returns hash with extension key" do
expect(line_item.launch_url_extension).to have_key(Lti::LineItem::AGS_EXT_LAUNCH_URL)
end
it "returns launch url in hash" do
expect(line_item.launch_url_extension[Lti::LineItem::AGS_EXT_LAUNCH_URL]).to eq url
end
end
context "with lti_link not matching assignment" do
let(:resource_link) { resource_link_model }
let(:line_item) { line_item_model resource_link: resource_link }

View File

@ -75,16 +75,37 @@ RSpec.describe Lti::IMS::LineItemsSerializer do
)
end
it "does not incude values that are nil" do
line_item.update!(resource_link: nil, tag: nil)
expect(described_class.new(line_item, line_item_id).as_json).to eq(
{
id: line_item_id,
scoreMaximum: line_item.score_maximum,
label: line_item.label,
resourceId: line_item.resource_id
}
)
context "with nil values" do
before do
line_item.update!(resource_link: nil, tag: nil)
end
it "does not incude values that are nil" do
expect(described_class.new(line_item, line_item_id).as_json).to eq(
{
id: line_item_id,
scoreMaximum: line_item.score_maximum,
label: line_item.label,
resourceId: line_item.resource_id
}
)
end
end
context "with submission_type extensions" do
before do
line_item.update!(extensions: { Lti::LineItem::AGS_EXT_SUBMISSION_TYPE => "extension" })
end
it "includes extension" do
expect(described_class.new(line_item, line_item_id).as_json).to include(Lti::LineItem::AGS_EXT_SUBMISSION_TYPE => "extension")
end
end
context "with launch_url extensions" do
it "includes extension" do
expect(described_class.new(line_item, line_item_id, true).as_json).to include(Lti::LineItem::AGS_EXT_LAUNCH_URL => tool.url)
end
end
end
end