Add Assignment#duplicate_of relation

This commit updates Assignment#duplicate to store a reference to the
original assignment on the duplicated assignment. Both the original
assignment and the new assignment's lti_resource_link_id are included
in the `assignment_created` live event emitted when the new assignment
is saved.

This allows LTI tools listening for Canvas live events to identify when
an assignment has been duplicated, and duplicate their own data
accordingly.

Closes QUIZ-3749

Test plan:

- Set up live events and tail the kinesis stream as per the instructions
  in doc/live_events.md
- Create an LTI assignment
- An `assignment_created` live event containing this assignment's
  lti_resource_link_id should appear in the kinesis log. Make a note of
  this lti_resource_link_id
- Duplicate this assignment by clicking the "kebab" menu and clicking
  "Duplicate"
- Check that another `assignment_created` live event appears in the
  kinesis log, containing both the original assignment's
  lti_resource_link_id and the new assignment's lti_resource_link_id

Change-Id: I64bdb9a2132e58c4e7be0ab7687c2c819a3587fd
Reviewed-on: https://gerrit.instructure.com/140877
Tested-by: Jenkins
QA-Review: Michael Hargiss <mhargiss@instructure.com>
Reviewed-by: Jeff Belser <jbelser@instructure.com>
Product-Review: Michael Hargiss <mhargiss@instructure.com>
This commit is contained in:
Omar Khan 2018-02-13 18:15:19 -06:00 committed by Michael Hargiss
parent d44fcac40e
commit 353b6e90d4
7 changed files with 95 additions and 8 deletions

View File

@ -72,6 +72,9 @@ class Assignment < ActiveRecord::Base
belongs_to :grading_standard
belongs_to :group_category
belongs_to :duplicate_of, class_name: 'Assignment', optional: true, inverse_of: :duplicates
has_many :duplicates, class_name: 'Assignment', inverse_of: :duplicate_of, foreign_key: 'duplicate_of_id'
has_many :assignment_configuration_tool_lookups, dependent: :delete_all
has_many :tool_settings_context_external_tools, through: :assignment_configuration_tool_lookups, source: :tool, source_type: 'ContextExternalTool'
has_many :line_items, inverse_of: :assignment, class_name: 'Lti::LineItem', dependent: :destroy
@ -199,6 +202,13 @@ class Assignment < ActiveRecord::Base
# Learning outcome alignments seem to get copied magically, possibly
# through the rubric
result.rubric_association = self.rubric_association.clone
# Link the duplicated assignment to this assignment
result.duplicate_of = self
# If this assignment uses an external tool, duplicate that too
result.external_tool_tag = self.external_tool_tag&.dup
result
end
@ -2580,6 +2590,11 @@ class Assignment < ActiveRecord::Base
@skip_due_date_validation = true
end
def lti_resource_link_id
return nil if external_tool_tag.blank?
ContextExternalTool.opaque_identifier_for(external_tool_tag, shard)
end
private
def due_date_ok?

View File

@ -35,12 +35,9 @@ module Exporters
end
def build_assignment_payload
external_tool_tag = @assignment.external_tool_tag
{
assignment: {
resource_link_id: ContextExternalTool.opaque_identifier_for(
external_tool_tag, @assignment.shard
),
resource_link_id: @assignment.lti_resource_link_id,
title: @quiz.title,
context_title: @quiz.context.name,
course_uuid: @course.uuid

View File

@ -0,0 +1,27 @@
#
# Copyright (C) 2018 - 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 AddDuplicateOfToAssignments < ActiveRecord::Migration[5.0]
tag :predeploy
disable_ddl_transaction!
def change
add_reference :assignments, :duplicate_of, type: :bigint, foreign_key: { to_table: :assignments }, index: false
add_index :assignments, :duplicate_of_id, where: 'duplicate_of_id IS NOT NULL', algorithm: :concurrently
end
end

View File

@ -192,7 +192,7 @@ module Api::V1::Assignment
hash['external_tool_tag_attributes'] = {
'url' => external_tool_tag.url,
'new_tab' => external_tool_tag.new_tab,
'resource_link_id' => ContextExternalTool.opaque_identifier_for(external_tool_tag, assignment.shard)
'resource_link_id' => assignment.lti_resource_link_id
}
hash['url'] = sessionless_launch_url(@context,
:launch_type => 'assessment',

View File

@ -172,7 +172,9 @@ module Canvas::LiveEvents
lock_at: assignment.lock_at,
updated_at: assignment.updated_at,
points_possible: assignment.points_possible,
lti_assignment_id: assignment.lti_context_id
lti_assignment_id: assignment.lti_context_id,
lti_resource_link_id: assignment.lti_resource_link_id,
lti_resource_link_id_duplicated_from: assignment.duplicate_of&.lti_resource_link_id
}
end

View File

@ -502,7 +502,9 @@ describe Canvas::LiveEvents do
unlock_at: assignment.unlock_at,
lock_at: assignment.lock_at,
points_possible: assignment.points_possible,
lti_assignment_id: assignment.lti_context_id
lti_assignment_id: assignment.lti_context_id,
lti_resource_link_id: assignment.lti_resource_link_id,
lti_resource_link_id_duplicated_from: assignment.duplicate_of&.lti_resource_link_id
})).once
Canvas::LiveEvents.assignment_created(assignment)
@ -526,7 +528,9 @@ describe Canvas::LiveEvents do
unlock_at: assignment.unlock_at,
lock_at: assignment.lock_at,
points_possible: assignment.points_possible,
lti_assignment_id: assignment.lti_context_id
lti_assignment_id: assignment.lti_context_id,
lti_resource_link_id: assignment.lti_resource_link_id,
lti_resource_link_id_duplicated_from: assignment.duplicate_of&.lti_resource_link_id
})).once
Canvas::LiveEvents.assignment_updated(assignment)

View File

@ -405,16 +405,29 @@ describe Assignment do
expect(new_assignment.rubric_association).not_to be_nil
expect(new_assignment.title).to eq "Wiki Assignment Copy"
expect(new_assignment.wiki_page.title).to eq "Wiki Assignment Copy"
expect(new_assignment.duplicate_of).to eq assignment
new_assignment.save!
new_assignment2 = assignment.duplicate
expect(new_assignment2.title).to eq "Wiki Assignment Copy 2"
new_assignment2.save!
expect(assignment.duplicates).to match_array [new_assignment, new_assignment2]
# Go back to the first new assignment to test something just ending in
# "Copy"
new_assignment3 = new_assignment.duplicate
expect(new_assignment3.title).to eq "Wiki Assignment Copy 3"
end
it "duplicates an assignment's external_tool_tag" do
assignment = @course.assignments.create!(
submission_types: 'external_tool',
external_tool_tag_attributes: { url: 'http://example.com/launch' },
**assignment_valid_attributes
)
new_assignment = assignment.duplicate
expect(new_assignment.external_tool_tag).to be_present
expect(new_assignment.external_tool_tag.content).to eq(assignment.external_tool_tag.content)
end
describe "#representatives" do
context "individual students" do
it "sorts by sortable_name" do
@ -4881,6 +4894,35 @@ describe Assignment do
end
end
describe '#lti_resource_link_id' do
subject { assignment.lti_resource_link_id }
context 'without external tool tag' do
let(:assignment) do
@course.assignments.create!(assignment_valid_attributes)
end
it { is_expected.to be_nil }
end
context 'with external tool tag' do
let(:assignment) do
@course.assignments.create!(submission_types: 'external_tool',
external_tool_tag_attributes: { url: 'http://example.com/launch' },
**assignment_valid_attributes)
end
it 'calls ContextExternalTool.opaque_identifier_for with the external tool tag and assignment shard' do
lti_resource_link_id = SecureRandom.hex
expect(ContextExternalTool).to receive(:opaque_identifier_for).with(
assignment.external_tool_tag,
assignment.shard
).and_return(lti_resource_link_id)
expect(assignment.lti_resource_link_id).to eq(lti_resource_link_id)
end
end
end
def setup_assignment_with_group
assignment_model(:group_category => "Study Groups", :course => @course)
@group = @a.context.groups.create!(:name => "Study Group 1", :group_category => @a.group_category)