Create ModuleAssignmentOverrides#index

List overrides that apply to a module, including the ids and names of
students/sections that are targeted by the override.

closes LF-651
flag = differentiated_modules

Test plan:
 - In a course, create a module
 - Create some assignment overrides for the module targeting both
   sections and students (see
   module_assignment_overrides_controller_spec lines 29-34)
 - GET
   /api/v1/courses/:course_id/modules/:module_id/assignment_overrides
 - Expect a list of overrides with section/student names and IDs
 - Make the request as a student
 - Expect unauthorized
 - Make a request with bad course or module IDs
 - Expect 404
 - Disable the flag and make the request
 - Expect 404

Change-Id: Ifdc812812734dcf58c573775cbb92ad21e4131c0
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/327379
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Robin Kuss <rkuss@instructure.com>
QA-Review: Robin Kuss <rkuss@instructure.com>
Product-Review: Jackson Howe <jackson.howe@instructure.com>
This commit is contained in:
Jackson Howe 2023-09-11 16:01:20 -06:00
parent cac04b454e
commit 280e5a0d6d
5 changed files with 287 additions and 1 deletions

View File

@ -0,0 +1,116 @@
# 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/>.
#
# @API Modules
# @subtopic Module Assignment Overrides
#
# If any active AssignmentOverrides exist on a ContextModule, then only students who have an
# applicable override can access the module and are assigned its items. AssignmentOverrides can
# be created for a (group of) student(s) or a section. *This module overrides feature is still
# under development and is not yet enabled.*
#
# @model ModuleAssignmentOverride
# {
# "id": "ModuleAssignmentOverride",
# "properties": {
# "id": {
# "description": "the ID of the assignment override",
# "example": 4355,
# "type": "integer"
# },
# "context_module_id": {
# "description": "the ID of the module the override applies to",
# "example": 567,
# "type": "integer"
# },
# "title": {
# "description": "the title of the override",
# "example": "Section 6",
# "type": "string"
# },
# "students": {
# "description": "an array of the override's target students (present only if the override targets an adhoc set of students)",
# "$ref": "OverrideTarget"
# },
# "course_section": {
# "description": "the override's target section (present only if the override targets a section)",
# "$ref": "OverrideTarget"
# }
# }
# }
#
# @model OverrideTarget
# {
# "id": "OverrideTarget",
# "properties": {
# "id": {
# "description": "the ID of the user or section that the override is targeting",
# "example": 7,
# "type": "integer"
# },
# "name": {
# "description": "the name of the user or section that the override is targeting",
# "example": "Section 6",
# "type": "string"
# }
# }
# }
class ModuleAssignmentOverridesController < ApplicationController
include Api::V1::ModuleAssignmentOverride
before_action :require_feature_flag # remove when differentiated_modules flag is removed
before_action :require_user
before_action :require_context
before_action :check_authorized_action
before_action :require_context_module
# @API List a module's overrides
#
# Returns a paginated list of AssignmentOverrides that apply to the ContextModule.
#
# Note: this API is still under development and will not function until the feature is enabled.
#
# @example_request
# curl https://<canvas>/api/v1/courses/:course_id/modules/:context_module_id/assignment_overrides \
# -H 'Authorization: Bearer <token>'
#
# @returns [ModuleAssignmentOverride]
def index
GuardRail.activate(:secondary) do
overrides = @context_module.assignment_overrides.active
paginated_overrides = Api.paginate(overrides, self, api_v1_module_assignment_overrides_index_url)
render json: module_assignment_overrides_json(paginated_overrides, @current_user)
end
end
private
def require_feature_flag
not_found unless Account.site_admin.feature_enabled? :differentiated_modules
end
def check_authorized_action
render_unauthorized_action unless @context.grants_any_right?(@current_user, :manage_content, :manage_course_content_edit)
end
def require_context_module
@context_module = @context.context_modules.not_deleted.find(params[:context_module_id])
end
end

View File

@ -333,7 +333,7 @@ class AssignmentOverride < ActiveRecord::Base
return Enrollment.none if overrides.empty? || user.nil?
override = overrides.first
(override.assignment || override.quiz).context.enrollments_visible_to(user)
(override.assignment || override.quiz || override.context_module).context.enrollments_visible_to(user)
end
OVERRIDDEN_DATES = %i[due_at unlock_at lock_at].freeze

View File

@ -1948,6 +1948,10 @@ CanvasRails::Application.routes.draw do
post "courses/:course_id/modules/items/:id/duplicate", action: :duplicate, as: :course_context_module_item_duplicate
end
scope(controller: :module_assignment_overrides) do
get "courses/:course_id/modules/:context_module_id/assignment_overrides", action: :index, as: "module_assignment_overrides_index"
end
scope(controller: "quizzes/quiz_assignment_overrides") do
get "courses/:course_id/quizzes/assignment_overrides", action: :index, as: "course_quiz_assignment_overrides"
get "courses/:course_id/new_quizzes/assignment_overrides", action: :new_quizzes, as: "course_new_quizzes_assignment_overrides"

View File

@ -0,0 +1,48 @@
# 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/>.
#
module Api::V1::ModuleAssignmentOverride
include Api::V1::Json
FIELDS = %i[id context_module_id title].freeze
def module_assignment_overrides_json(overrides, user)
adhoc_overrides = overrides.select { |override| override.set_type == "ADHOC" }
visible_users_ids = ::AssignmentOverride.visible_enrollments_for(overrides.compact, user).select(:user_id)
if adhoc_overrides.any? { |override| !override.preloaded_student_ids }
AssignmentOverrideApplicator.preload_student_ids_for_adhoc_overrides(adhoc_overrides, visible_users_ids)
end
user_names = User.where(id: adhoc_overrides.flat_map(&:preloaded_student_ids)).pluck(:id, :name).to_h
overrides.map { |override| module_assignment_override_json(override, user_names) }
end
private
def module_assignment_override_json(override, user_names)
api_json(override, @current_user, session, only: FIELDS).tap do |json|
case override.set_type
when "ADHOC"
json[:students] = override.preloaded_student_ids.map { |user_id| { id: user_id, name: user_names[user_id] } }
when "CourseSection"
json[:course_section] = { id: override.set.id, name: override.set.name }
end
end
end
end

View File

@ -0,0 +1,118 @@
# 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/>.
#
describe ModuleAssignmentOverridesController do
before :once do
Account.site_admin.enable_feature!(:differentiated_modules)
course_with_teacher(active_all: true, course_name: "Awesome Course")
@student1 = student_in_course(active_all: true, name: "Student 1").user
@student2 = student_in_course(active_all: true, name: "Student 2").user
@student3 = student_in_course(active_all: true, name: "Student 3").user
@module1 = @course.context_modules.create!(name: "Module 1")
@section_override1 = @module1.assignment_overrides.create!(set_type: "CourseSection", set_id: @course.course_sections.first)
@adhoc_override1 = @module1.assignment_overrides.create!(set_type: "ADHOC")
@adhoc_override1.assignment_override_students.create!(user: @student1)
@adhoc_override1.assignment_override_students.create!(user: @student2)
@adhoc_override2 = @module1.assignment_overrides.create!(set_type: "ADHOC")
@adhoc_override2.assignment_override_students.create!(user: @student3)
end
before do
user_session(@teacher)
end
describe "GET 'index'" do
it "returns a list of module assignment overrides" do
get :index, params: { course_id: @course.id, context_module_id: @module1.id }
expect(response).to be_successful
json = json_parse(response.body)
expect(json.length).to be 3
expect(json[0]["id"]).to be @section_override1.id
expect(json[0]["context_module_id"]).to be @module1.id
expect(json[0]["title"]).to eq "Awesome Course"
expect(json[0]["course_section"]["id"]).to eq @course.course_sections.first.id
expect(json[0]["course_section"]["name"]).to eq "Awesome Course"
expect(json[1]["id"]).to be @adhoc_override1.id
expect(json[1]["context_module_id"]).to be @module1.id
expect(json[1]["title"]).to eq "No Title"
expect(json[1]["students"].length).to eq 2
expect(json[1]["students"][0]["id"]).to eq @student1.id
expect(json[1]["students"][0]["name"]).to eq "Student 1"
expect(json[1]["students"][1]["id"]).to eq @student2.id
expect(json[1]["students"][1]["name"]).to eq "Student 2"
expect(json[2]["id"]).to be @adhoc_override2.id
expect(json[2]["context_module_id"]).to be @module1.id
expect(json[2]["title"]).to eq "No Title"
expect(json[2]["students"].length).to eq 1
expect(json[2]["students"][0]["id"]).to eq @student3.id
expect(json[2]["students"][0]["name"]).to eq "Student 3"
end
it "does not include deleted assignment overrides" do
@adhoc_override2.update!(workflow_state: "deleted")
get :index, params: { course_id: @course.id, context_module_id: @module1.id }
expect(response).to be_successful
json = json_parse(response.body)
expect(json.pluck("id")).to contain_exactly(@section_override1.id, @adhoc_override1.id)
end
it "returns 404 if the course doesn't exist" do
get :index, params: { course_id: 0, context_module_id: @module1.id }
expect(response).to be_not_found
end
it "returns 404 if the module is deleted or nonexistent" do
@module1.update!(workflow_state: "deleted")
get :index, params: { course_id: @course.id, context_module_id: @module1.id }
expect(response).to be_not_found
@module1.assignment_override_students.each(&:delete)
@module1.assignment_overrides.each(&:delete)
@module1.delete
get :index, params: { course_id: @course.id, context_module_id: @module1.id }
expect(response).to be_not_found
end
it "returns 404 if the module is in a different course" do
course2 = course_with_teacher(active_all: true, user: @teacher).course
course2.context_modules.create!
get :index, params: { course_id: course2, context_module_id: @module1.id }
expect(response).to be_not_found
end
it "returns 404 if the differentiated_modules flag is disabled" do
Account.site_admin.disable_feature!(:differentiated_modules)
get :index, params: { course_id: @course.id, context_module_id: @module1.id }
expect(response).to be_not_found
end
it "returns unauthorized if the user doesn't have manage_course_content_edit permission" do
student = student_in_course.user
user_session(student)
get :index, params: { course_id: @course.id, context_module_id: @module1.id }
expect(response).to be_unauthorized
end
end
end