canvas-lms/spec/apis/v1/enrollments_api_spec.rb

2929 lines
128 KiB
Ruby

# frozen_string_literal: true
#
# Copyright (C) 2011 - 2016 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/>.
#
require File.expand_path(File.dirname(__FILE__) + '/../api_spec_helper')
require File.expand_path(File.dirname(__FILE__) + '/../../sharding_spec_helper')
describe EnrollmentsApiController, type: :request do
describe "enrollment creation" do
context "an admin user" do
before :once do
account_admin_user(:active_all => true)
course_factory(active_course: true)
@unenrolled_user = user_with_pseudonym
@section = @course.course_sections.create!
@path = "/api/v1/courses/#{@course.id}/enrollments"
@path_options = { :controller => 'enrollments_api', :action => 'create', :format => 'json', :course_id => @course.id.to_s }
@user = @admin
end
it "creates a new student enrollment" do
json = api_call :post, @path, @path_options,
{
:enrollment => {
:user_id => @unenrolled_user.id,
:type => 'StudentEnrollment',
:enrollment_state => 'active',
:course_section_id => @section.id,
:limit_privileges_to_course_section => true,
:start_at => nil,
:end_at => nil
}
}
new_enrollment = Enrollment.find(json['id'])
expect(json).to eq({
'root_account_id' => @course.account.id,
'id' => new_enrollment.id,
'user_id' => @unenrolled_user.id,
'course_section_id' => @section.id,
'limit_privileges_to_course_section' => true,
'enrollment_state' => 'active',
'course_id' => @course.id,
'sis_import_id' => nil,
'type' => 'StudentEnrollment',
'role' => 'StudentEnrollment',
'role_id' => student_role.id,
'html_url' => course_user_url(@course, @unenrolled_user),
'grades' => {
'html_url' => course_student_grades_url(@course, @unenrolled_user),
'final_score' => nil,
'current_score' => nil,
'final_grade' => nil,
'current_grade' => nil,
'unposted_final_score' => nil,
'unposted_current_score' => nil,
'unposted_final_grade' => nil,
'unposted_current_grade' => nil
},
'associated_user_id' => nil,
'updated_at' => new_enrollment.updated_at.xmlschema,
'created_at' => new_enrollment.created_at.xmlschema,
'last_activity_at' => nil,
'last_attended_at' => nil,
'total_activity_time' => 0,
'sis_account_id' => @course.account.sis_source_id,
'sis_course_id' => @course.sis_source_id,
'course_integration_id' => @course.integration_id,
'sis_section_id' => @section.sis_source_id,
'sis_user_id' => @unenrolled_user.pseudonym.sis_user_id,
'section_integration_id' => @section.integration_id,
'start_at' => nil,
'end_at' => nil
})
expect(new_enrollment.root_account_id).to eql @course.account.id
expect(new_enrollment.user_id).to eql @unenrolled_user.id
expect(new_enrollment.course_section_id).to eql @section.id
expect(new_enrollment.limit_privileges_to_course_section).to eql true
expect(new_enrollment.workflow_state).to eql 'active'
expect(new_enrollment.course_id).to eql @course.id
expect(new_enrollment.self_enrolled).to eq nil
expect(new_enrollment).to be_an_instance_of StudentEnrollment
end
it "does not allow enrolling a student view student" do
c2 = Account.default.courses.create!
user = c2.student_view_student
json = api_call :post, @path, @path_options,
{
:enrollment => {
:user_id => user.id,
:type => 'StudentEnrollment',
:enrollment_state => 'active',
:course_section_id => @section.id,
:limit_privileges_to_course_section => true,
:start_at => nil,
:end_at => nil
}
}, {}, expected_status: 400
expect(@section.enrollments.count).to eq 0
end
it "does not allow enrolling a user as a student view student" do
json = api_call :post, @path, @path_options,
{
:enrollment => {
:user_id => @unenrolled_user.id,
:type => 'StudentViewEnrollment',
:enrollment_state => 'active',
:course_section_id => @section.id,
:limit_privileges_to_course_section => true,
:start_at => nil,
:end_at => nil
}
}, {}, expected_status: 400
expect(@section.enrollments.count).to eq 0
end
it "accepts sis_section_id" do
@section.update_attribute(:sis_source_id, 'sis_id')
json = api_call :post, @path, @path_options,
{
:enrollment => {
:user_id => @unenrolled_user.id,
:type => 'StudentEnrollment',
:enrollment_state => 'active',
:course_section_id => 'sis_section_id:sis_id',
:limit_privileges_to_course_section => true,
:start_at => nil,
:end_at => nil
}
}
new_enrollment = Enrollment.find(json['id'])
expect(new_enrollment.course_section).to eq @section
end
it "is unauthorized for users without manage_students permission (non-granular)" do
@course.root_account.disable_feature!(:granular_permissions_manage_users)
@course.account.role_overrides.create!(role: admin_role, enabled: false, permission: :manage_students)
json = api_call :post, @path, @path_options,
{
:enrollment => {
:user_id => @unenrolled_user.id,
:type => 'StudentEnrollment',
:enrollment_state => 'active',
:course_section_id => @section.id,
:limit_privileges_to_course_section => true,
:start_at => nil,
:end_at => nil
}
}, {}, { :expected_status => 401 }
end
it "is unauthorized for users without add_student_to_course permission (granular)" do
@course.root_account.enable_feature!(:granular_permissions_manage_users)
@course.account.role_overrides.create!(role: admin_role, enabled: false, permission: :manage_students)
@course.account.role_overrides.create!(role: admin_role, enabled: false, permission: :add_student_to_course)
json = api_call :post, @path, @path_options,
{
:enrollment => {
:user_id => @unenrolled_user.id,
:type => 'StudentEnrollment',
:enrollment_state => 'active',
:course_section_id => @section.id,
:limit_privileges_to_course_section => true,
:start_at => nil,
:end_at => nil
}
}, {}, { :expected_status => 401 }
end
it "creates a new teacher enrollment" do
json = api_call :post, @path, @path_options,
{
:enrollment => {
:user_id => @unenrolled_user.id,
:type => 'TeacherEnrollment',
:enrollment_state => 'active',
:course_section_id => @section.id,
:limit_privileges_to_course_section => true
}
}
enrollment = Enrollment.find(json['id'])
expect(enrollment).to be_an_instance_of TeacherEnrollment
expect(enrollment.workflow_state).to eq 'active'
expect(enrollment.course_section).to eq @section
expect(enrollment.limit_privileges_to_course_section).to eq true
end
it "interprets 'false' correctly" do
json = api_call :post, @path, @path_options,
{
:enrollment => {
:user_id => @unenrolled_user.id,
:type => 'TeacherEnrollment',
:limit_privileges_to_course_section => 'false'
}
}
expect(Enrollment.find(json['id']).limit_privileges_to_course_section).to eq false
end
it "adds a section limitation after the fact" do
enrollment = @course.enroll_teacher @unenrolled_user
json = api_call :post, @path, @path_options,
{
:enrollment => {
:user_id => @unenrolled_user.id,
:type => 'TeacherEnrollment',
:limit_privileges_to_course_section => 'true'
}
}
expect(json['id']).to eq enrollment.id
expect(enrollment.reload.limit_privileges_to_course_section).to eq true
end
it "creates a new ta enrollment" do
json = api_call :post, @path, @path_options,
{
:enrollment => {
:user_id => @unenrolled_user.id,
:type => 'TaEnrollment',
:enrollment_state => 'active',
:course_section_id => @section.id,
:limit_privileges_to_course_section => true
}
}
expect(Enrollment.find(json['id'])).to be_an_instance_of TaEnrollment
end
it "creates a new observer enrollment" do
json = api_call :post, @path, @path_options,
{
:enrollment => {
:user_id => @unenrolled_user.id,
:type => 'ObserverEnrollment',
:enrollment_state => 'invited',
:course_section_id => @section.id,
:limit_privileges_to_course_section => true
}
}
enrollment = Enrollment.find(json['id'])
expect(enrollment).to be_an_instance_of ObserverEnrollment
expect(enrollment.workflow_state).to eq 'invited'
end
it "does not default observer enrollments to 'active' state if the user is not registered" do
json = api_call :post, @path, @path_options,
{
:enrollment => {
:user_id => @unenrolled_user.id,
:type => 'ObserverEnrollment',
:course_section_id => @section.id,
:limit_privileges_to_course_section => true
}
}
enrollment = Enrollment.find(json['id'])
expect(enrollment).to be_an_instance_of ObserverEnrollment
expect(enrollment.workflow_state).to eq 'invited'
end
it "defaults observer enrollments to 'active' state if the user is registered" do
@unenrolled_user.register!
json = api_call :post, @path, @path_options,
{
:enrollment => {
:user_id => @unenrolled_user.id,
:type => 'ObserverEnrollment',
:course_section_id => @section.id,
:limit_privileges_to_course_section => true
}
}
enrollment = Enrollment.find(json['id'])
expect(enrollment).to be_an_instance_of ObserverEnrollment
expect(enrollment.workflow_state).to eq 'active'
end
it "does not create a new observer enrollment for self" do
raw_api_call :post, @path, @path_options,
{
:enrollment => {
:user_id => @unenrolled_user.id,
:type => 'ObserverEnrollment',
:enrollment_state => 'active',
:associated_user_id => @unenrolled_user.id,
:course_section_id => @section.id,
:limit_privileges_to_course_section => true
}
}
expect(response.code).to eql '400'
expect(JSON.parse(response.body)).to eq(
{ "errors" => { "associated_user_id" => [{ "attribute" => "associated_user_id", "type" => "Cannot observe yourself", "message" => "Cannot observe yourself" }] } }
)
end
it "defaults new enrollments to the 'invited' state in the default section" do
json = api_call :post, @path, @path_options,
{
:enrollment => {
:user_id => @unenrolled_user.id,
:type => 'StudentEnrollment'
}
}
e = Enrollment.find(json['id'])
expect(e.workflow_state).to eql 'invited'
expect(e.course_section).to eql @course.default_section
end
it "defaults new enrollments to the 'creation_pending' state for unpublished courses" do
@course.update_attribute(:workflow_state, 'claimed')
json = api_call :post, @path, @path_options,
{
:enrollment => {
:user_id => @unenrolled_user.id,
:type => 'StudentEnrollment'
}
}
e = Enrollment.find(json['id'])
expect(e.workflow_state).to eql 'creation_pending'
expect(e.course_section).to eql @course.default_section
end
it "throws an error if no params are given" do
raw_api_call :post, @path, @path_options, { :enrollment => {} }
expect(response.code).to eql '400'
expect(JSON.parse(response.body)).to eq({
'message' => 'No parameters given'
})
end
it "assumes a StudentEnrollment if no type is given" do
api_call :post, @path, @path_options, { :enrollment => { :user_id => @unenrolled_user.id } }
expect(JSON.parse(response.body)['type']).to eql 'StudentEnrollment'
end
it "allows creating self-enrollments" do
json = api_call :post, @path, @path_options, { :enrollment => { :user_id => @unenrolled_user.id, :self_enrolled => true } }
expect(@unenrolled_user.enrollments.find(json['id']).self_enrolled).to eq(true)
end
it "returns an error if an invalid type is given" do
raw_api_call :post, @path, @path_options, { :enrollment => { :user_id => @unenrolled_user.id, :type => 'PandaEnrollment' } }
expect(JSON.parse(response.body)['message']).to eql 'Invalid type'
end
it "enrolls a designer" do
json = api_call :post, @path, @path_options, { :enrollment => { :user_id => @unenrolled_user.id, :type => 'DesignerEnrollment' } }
expect(json['type']).to eql 'DesignerEnrollment'
expect(@unenrolled_user.enrollments.find(json['id'])).to be_an_instance_of(DesignerEnrollment)
end
it "returns an error if no user_id is given" do
raw_api_call :post, @path, @path_options, { :enrollment => { :type => 'StudentEnrollment' } }
expect(response.code).to eql '400'
expect(JSON.parse(response.body)).to eq({
'message' => "Can't create an enrollment without a user. Include enrollment[user_id] to create an enrollment"
})
end
it "enrolls to the right section using the section-specific URL" do
@path = "/api/v1/sections/#{@section.id}/enrollments"
@path_options = { :controller => 'enrollments_api', :action => 'create', :format => 'json', :section_id => @section.id.to_s }
json = api_call :post, @path, @path_options, { :enrollment => { :user_id => @unenrolled_user.id, } }
expect(Enrollment.find(json['id']).course_section).to eql @section
end
it "does not notify by default" do
expect_any_instance_of(StudentEnrollment).to receive(:save_without_broadcasting).at_least(:once)
api_call(:post, @path, @path_options, {
:enrollment => {
:user_id => @unenrolled_user.id,
:enrollment_state => 'active'
}
})
end
it "optionallies send notifications" do
expect_any_instance_of(StudentEnrollment).to receive(:save).at_least(:once)
api_call(:post, @path, @path_options, {
:enrollment => {
:user_id => @unenrolled_user.id,
:enrollment_state => 'active',
:notify => true
}
})
end
it "does not allow enrollments to be added to a hard-concluded course" do
@course.complete
raw_api_call :post, @path, @path_options, {
:enrollment => {
:user_id => @unenrolled_user.id,
:type => 'StudentEnrollment',
:enrollment_state => 'active',
:course_section_id => @section.id,
:limit_privileges_to_course_section => true
}
}
expect(JSON.parse(response.body)['message']).to eql 'Can\'t add an enrollment to a concluded course.'
end
it "does not allow enrollments to be added to a soft-concluded course" do
@course.start_at = 2.days.ago
@course.conclude_at = 1.day.ago
@course.restrict_enrollments_to_course_dates = true
@course.save!
raw_api_call :post, @path, @path_options, {
:enrollment => {
:user_id => @unenrolled_user.id,
:type => 'StudentEnrollment',
:enrollment_state => 'active',
:course_section_id => @section.id,
:limit_privileges_to_course_section => true
}
}
expect(JSON.parse(response.body)['message']).to eql 'Can\'t add an enrollment to a concluded course.'
end
it "allows enrollments to be added to an active section of a concluded course if the user is already enrolled" do
other_section = @course.course_sections.create!
@course.enroll_user(@unenrolled_user, "StudentEnrollment", :section => other_section)
@course.start_at = 2.days.ago
@course.conclude_at = 1.day.ago
@course.restrict_enrollments_to_course_dates = true
@course.save!
@section.end_at = 1.day.from_now
@section.restrict_enrollments_to_section_dates = true
@section.save!
expect(@section).to_not be_concluded
api_call :post, @path, @path_options, {
:enrollment => {
:user_id => @unenrolled_user.id,
:type => 'StudentEnrollment',
:enrollment_state => 'active',
:course_section_id => @section.id,
:limit_privileges_to_course_section => true
}
}
end
it "does not allow enrollments to be added to an active section of a concluded course if the user is not already enrolled" do
@course.start_at = 2.days.ago
@course.conclude_at = 1.day.ago
@course.restrict_enrollments_to_course_dates = true
@course.save!
@section.end_at = 1.day.from_now
@section.restrict_enrollments_to_section_dates = true
@section.save!
raw_api_call :post, @path, @path_options, {
:enrollment => {
:user_id => @unenrolled_user.id,
:type => 'StudentEnrollment',
:enrollment_state => 'active',
:course_section_id => @section.id,
:limit_privileges_to_course_section => true
}
}
expect(JSON.parse(response.body)['message']).to eql 'Can\'t add an enrollment to a concluded course.'
end
it "does not enroll a user lacking a pseudonym on the course's account" do
foreign_user = user_factory
api_call_as_user @admin, :post, @path, @path_options, { :enrollment => { :user_id => foreign_user.id } }, {},
{ expected_status: 404 }
end
it "does not allow adding users to a template course" do
@course.update!(template: true)
api_call :post, @path, @path_options, { :enrollment => { :user_id => @unenrolled_user.id } }, {},
{ expected_status: 401 }
end
context "custom course-level roles" do
before :once do
@course_role = @course.root_account.roles.build(:name => 'newrole')
@course_role.base_role_type = 'TeacherEnrollment'
@course_role.save!
end
it "sets role_id and type for a new enrollment if role is specified" do
json = api_call :post, @path, @path_options,
{
:enrollment => {
:user_id => @unenrolled_user.id,
:role => 'newrole',
:enrollment_state => 'active',
:course_section_id => @section.id,
:limit_privileges_to_course_section => true
}
}
expect(Enrollment.find(json['id'])).to be_an_instance_of TeacherEnrollment
expect(Enrollment.find(json['id']).role_id).to eq @course_role.id
expect(json['role']).to eq 'newrole'
expect(json['role_id']).to eq @course_role.id
end
it "returns an error if type is specified but does not the role's base_role_type" do
json = api_call :post, @path, @path_options, {
:enrollment => {
:user_id => @unenrolled_user.id,
:role => 'newrole',
:type => 'StudentEnrollment',
:enrollment_state => 'active',
:course_section_id => @section.id,
:limit_privileges_to_course_section => true
}
}, {}, :expected_status => 400
expect(json['message']).to eql 'The specified type must match the base type for the role'
end
it "returns an error if role is specified but is invalid" do
json = api_call :post, @path, @path_options, {
:enrollment => {
:user_id => @unenrolled_user.id,
:role => 'badrole',
:enrollment_state => 'active',
:course_section_id => @section.id,
:limit_privileges_to_course_section => true
}
}, {}, :expected_status => 400
expect(json['message']).to eql 'Invalid role'
end
it "returns an error if role is specified but is inactive" do
@course_role.deactivate
json = api_call :post, @path, @path_options, {
:enrollment => {
:user_id => @unenrolled_user.id,
:role => 'newrole',
:enrollment_state => 'active',
:course_section_id => @section.id,
:limit_privileges_to_course_section => true
}
}, {}, :expected_status => 400
expect(json['message']).to eql 'Cannot create an enrollment with this role because it is inactive.'
end
it "returns a suitable error if role is specified but is deleted" do
@course_role.destroy
json = api_call :post, @path, @path_options, {
:enrollment => {
:user_id => @unenrolled_user.id,
:role => 'newrole',
:enrollment_state => 'active',
:course_section_id => @section.id,
:limit_privileges_to_course_section => true
}
}, {}, :expected_status => 400
expect(json['message']).to eql 'Invalid role'
end
it "accepts base roles in the role parameter" do
json = api_call :post, @path, @path_options,
{
:enrollment => {
:user_id => @unenrolled_user.id,
:role => 'ObserverEnrollment',
:enrollment_state => 'active',
:course_section_id => @section.id,
:limit_privileges_to_course_section => true
}
}
expect(Enrollment.find(json['id'])).to be_an_instance_of ObserverEnrollment
end
it "derives roles from parent accounts" do
sub_account = Account.create!(:name => 'sub', :parent_account => @course.account)
course_factory(:account => sub_account)
expect(@course.account.roles.active.where(:name => 'newrole').first).to be_nil
course_role = @course.account.get_course_role_by_name('newrole')
expect(course_role).to_not be_nil
@path = "/api/v1/courses/#{@course.id}/enrollments"
@path_options = { :controller => 'enrollments_api', :action => 'create', :format => 'json', :course_id => @course.id.to_s }
@section = @course.course_sections.create!
json = api_call :post, @path, @path_options,
{
:enrollment => {
:user_id => @unenrolled_user.id,
:role_id => course_role.id,
:enrollment_state => 'active',
:course_section_id => @section.id,
:limit_privileges_to_course_section => true
}
}
expect(Enrollment.find(json['id'])).to be_an_instance_of TeacherEnrollment
expect(Enrollment.find(json['id']).role_id).to eq course_role.id
expect(json['role']).to eq 'newrole'
expect(json['role_id']).to eq course_role.id
end
end
end
context "a teacher" do
before :once do
course_with_teacher(:active_all => true)
@course_with_teacher = @course
@course_wo_teacher = course_factory
@course = @course_with_teacher
@unenrolled_user = user_with_pseudonym
@section = @course.course_sections.create
@path = "/api/v1/courses/#{@course.id}/enrollments"
@path_options = { :controller => 'enrollments_api', :action => 'create', :format => 'json', :course_id => @course.id.to_s }
@user = @teacher
end
it "creates enrollments for its own class" do
json = api_call :post, @path, @path_options,
{
:enrollment => {
:user_id => @unenrolled_user.id,
:type => 'StudentEnrollment',
:enrollment_state => 'active',
:course_section_id => @section.id,
:limit_privileges_to_course_section => true
}
}
new_enrollment = Enrollment.find(json['id'])
expect(json).to eq({
'root_account_id' => @course.account.id,
'id' => new_enrollment.id,
'user_id' => @unenrolled_user.id,
'course_section_id' => @section.id,
'limit_privileges_to_course_section' => true,
'enrollment_state' => 'active',
'course_id' => @course.id,
'type' => 'StudentEnrollment',
'role' => 'StudentEnrollment',
'role_id' => student_role.id,
'html_url' => course_user_url(@course, @unenrolled_user),
'grades' => {
'html_url' => course_student_grades_url(@course, @unenrolled_user),
'final_score' => nil,
'current_score' => nil,
'final_grade' => nil,
'current_grade' => nil,
'unposted_final_score' => nil,
'unposted_current_score' => nil,
'unposted_final_grade' => nil,
'unposted_current_grade' => nil
},
'associated_user_id' => nil,
'updated_at' => new_enrollment.updated_at.xmlschema,
'created_at' => new_enrollment.created_at.xmlschema,
'last_activity_at' => nil,
'last_attended_at' => nil,
'total_activity_time' => 0,
'sis_account_id' => @course.account.sis_source_id,
'sis_course_id' => @course.sis_source_id,
'course_integration_id' => @course.integration_id,
'sis_section_id' => @section.sis_source_id,
'sis_user_id' => @unenrolled_user.pseudonym.sis_user_id,
'section_integration_id' => @section.integration_id,
'start_at' => nil,
'end_at' => nil
})
expect(new_enrollment.root_account_id).to eql @course.account.id
expect(new_enrollment.user_id).to eql @unenrolled_user.id
expect(new_enrollment.course_section_id).to eql @section.id
expect(new_enrollment.limit_privileges_to_course_section).to eql true
expect(new_enrollment.workflow_state).to eql 'active'
expect(new_enrollment.course_id).to eql @course.id
expect(new_enrollment).to be_an_instance_of StudentEnrollment
end
it "does not create an enrollment for another class" do
raw_api_call :post, "/api/v1/courses/#{@course_wo_teacher.id}/enrollments", @path_options.merge(:course_id => @course_wo_teacher.id.to_s),
{
:enrollment => {
:user_id => @unenrolled_user.id,
:type => 'StudentEnrollment'
}
}
expect(response.code).to eql '401'
end
end
context "a student" do
before :once do
course_with_student(:active_all => true)
@unenrolled_user = user_with_pseudonym
@path = "/api/v1/courses/#{@course.id}/enrollments"
@path_options = { :controller => 'enrollments_api', :action => 'create', :format => 'json', :course_id => @course.id.to_s }
@user = @student
end
it "returns 401 Unauthorized" do
raw_api_call :post, @path, @path_options,
{
:enrollment => {
:user_id => @unenrolled_user,
:type => 'StudentEnrollment'
}
}
expect(response.code).to eql '401'
end
end
context "self enrollment" do
before :once do
Account.default.allow_self_enrollment!
course_factory(active_all: true)
@course.update_attribute(:self_enrollment, true)
@unenrolled_user = user_with_pseudonym
@path = "/api/v1/courses/#{@course.id}/enrollments"
@path_options = { controller: 'enrollments_api', action: 'create', format: 'json', course_id: @course.id.to_s }
end
it "requires a logged-in user" do
@user = nil
raw_api_call :post, @path, @path_options,
{
enrollment: {
user_id: 'self',
self_enrollment_code: @course.self_enrollment_code
}
}
expect(response.code).to eql '401'
end
it "requires a valid code and user" do
raw_api_call :post, @path, @path_options,
{
enrollment: {
user_id: 'invalid',
self_enrollment_code: 'invalid'
}
}
expect(response.code).to eql '400'
json = JSON.parse(response.body)
expect(json["message"]).to be_include "enrollment[self_enrollment_code] is invalid"
expect(json["message"]).to be_include "enrollment[user_id] must be 'self' when self-enrolling"
end
it "requires the course to be in a valid state" do
MasterCourses::MasterTemplate.set_as_master_course(@course)
raw_api_call :post, @path, @path_options,
{ enrollment: { user_id: 'self', self_enrollment_code: @course.self_enrollment_code } }
expect(response.code).to eql '400'
json = JSON.parse(response.body)
expect(json["message"]).to be_include "course is not open for self-enrollment"
end
it "lets anyone self-enroll" do
json = api_call :post, @path, @path_options,
{
enrollment: {
user_id: 'self',
self_enrollment_code: @course.self_enrollment_code
}
}
new_enrollment = Enrollment.find(json['id'])
expect(new_enrollment.user_id).to eq @unenrolled_user.id
expect(new_enrollment.type).to eq 'StudentEnrollment'
expect(new_enrollment).to be_active
expect(new_enrollment).to be_self_enrolled
end
it "does not let anyone self-enroll if account disables it" do
account = @course.root_account
account.settings.delete(:self_enrollment)
account.save!
json = raw_api_call :post, @path, @path_options,
{
enrollment: {
user_id: 'self',
self_enrollment_code: @course.self_enrollment_code
}
}
expect(response.code).to eql '400'
end
it "does not allow self-enrollment in a concluded course" do
@course.update(:start_at => 2.days.ago, :conclude_at => 1.day.ago,
:restrict_enrollments_to_course_dates => true)
json = raw_api_call :post, @path, @path_options,
{ enrollment: { user_id: 'self', self_enrollment_code: @course.self_enrollment_code } }
expect(response.code).to eql '400'
expect(response.body).to include("concluded")
end
context "sharding" do
specs_require_sharding
it "properly restores an existing enrollment when self-enrolling a cross-shard user" do
@shard1.activate { @cs_user = user_with_pseudonym(:active_all => true) }
enrollment = @course.enroll_student(@cs_user)
enrollment.destroy
@me = @cs_user
json = api_call :post, @path, @path_options,
{
enrollment: {
user_id: 'self',
self_enrollment_code: @course.self_enrollment_code
}
}, {}, { :expected_status => 200 }
expect(json['id']).to eq enrollment.id
expect(enrollment.reload).to be_active
end
end
end
end
describe "enrollment listing" do
before :once do
course_with_student(:active_all => true, :user => user_with_pseudonym)
@group = @course.groups.create!(:name => "My Group")
@group.add_user(@student, 'accepted', true)
@teacher = User.create(:name => 'Señor Chang')
@teacher.pseudonyms.create(:unique_id => 'chang@example.com')
@course.enroll_teacher(@teacher)
User.all.each { |u| u.destroy unless u.pseudonym.present? }
@path = "/api/v1/courses/#{@course.id}/enrollments"
@user_path = "/api/v1/users/#{@user.id}/enrollments"
@enroll_path = "/api/v1/accounts/#{@enrollment.root_account_id}/enrollments"
@params = { :controller => "enrollments_api", :action => "index", :course_id => @course.id.to_param, :format => "json" }
@enroll_params = { :controller => "enrollments_api", :action => "show", :account_id => @enrollment.root_account_id, :id => @enrollment.id, :format => "json" }
@user_params = { :controller => "enrollments_api", :action => "index", :user_id => @user.id.to_param, :format => "json" }
@section = @course.course_sections.create!
end
it "deterministicallies order enrollments for pagination" do
allow_any_instance_of(EnrollmentsApiController).to receive(:use_bookmarking?).and_return(true)
enrollment_num = 10
enrollment_num.times do
u = user_with_pseudonym(name: "John Smith", sortable_name: "Smith, John")
@course.enroll_user(u, 'StudentEnrollment', :enrollment_state => 'active')
end
found_enrollment_ids = []
enrollment_num.times do |i|
json = if i == 0
api_call(:get, "/api/v1/courses/#{@course.id}/enrollments?per_page=1",
:controller => "enrollments_api", :action => "index", :format => "json",
:course_id => @course.id.to_s, :per_page => 1)
else
follow_pagination_link('next', { :controller => 'enrollments_api',
:action => 'index', :format => 'json', :course_id => @course.id.to_s })
end
id = json[0]["id"]
id_already_found = found_enrollment_ids.include?(id)
expect(id_already_found).to be_falsey
found_enrollment_ids << id
end
end
it "deterministicallies order enrollments for pagination with bookmarking not enabled" do
allow_any_instance_of(EnrollmentsApiController).to receive(:use_bookmarking?).and_return(false)
enrollment_num = 10
enrollment_num.times do
u = user_with_pseudonym(name: "John Smith", sortable_name: "Smith, John")
@course.enroll_user(u, 'StudentEnrollment', :enrollment_state => 'active')
end
found_enrollment_ids = []
enrollment_num.times do |i|
json = if i == 0
api_call(:get, "/api/v1/courses/#{@course.id}/enrollments?per_page=1",
:controller => "enrollments_api", :action => "index", :format => "json",
:course_id => @course.id.to_s, :per_page => 1)
else
follow_pagination_link('next', { :controller => 'enrollments_api',
:action => 'index', :format => 'json', :course_id => @course.id.to_s })
end
id = json[0]["id"]
id_already_found = found_enrollment_ids.include?(id)
expect(id_already_found).to be_falsey
found_enrollment_ids << id
end
end
context "filtering by SIS IDs" do
it "returns an error message with insufficient permissions" do
@params[:sis_user_id] = '12345'
json = api_call(:get, @path, @params, {}, {}, { expected_status: 400 })
expect(json['message']).to eq 'Insufficient permissions to filter by SIS fields'
end
end
context "grading periods" do
let(:group_helper) { Factories::GradingPeriodGroupHelper.new }
let(:grading_period_group) { group_helper.legacy_create_for_course(@course) }
let(:now) { Time.zone.now }
before :once do
@first_grading_period = grading_period_group.grading_periods.create!(
title: 'first',
start_date: 2.months.ago(now),
end_date: now
)
@last_grading_period = grading_period_group.grading_periods.create!(
title: 'second',
start_date: now,
end_date: 2.months.from_now(now)
)
@assignment_in_first_period = @course.assignments.create!(
due_at: 2.days.ago(now),
points_possible: 10
)
@assignment_in_last_period = @course.assignments.create!(
due_at: 1.day.from_now(now),
points_possible: 10
)
end
describe "user endpoint" do
let!(:enroll_student_in_the_course) do
student_in_course({ course: @course, user: @user })
end
it "works for users" do
@user_params[:grading_period_id] = @first_grading_period.id
raw_api_call(:get, @user_path, @user_params)
expect(response).to be_ok
end
it "filters to terms for users" do
term = EnrollmentTerm.create!(name: 'fall', root_account_id: @course.root_account_id)
course = Course.create!(enrollment_term_id: term.id, root_account_id: @course.root_account_id, workflow_state: 'available')
e = course.enroll_user(@student)
@user_params[:enrollment_term_id] = term.id
json = api_call(:get, @user_path, @user_params)
expect(json.length).to eq(1)
expect(json.first['id']).to eq e.id
end
it "returns an error if the user is not in the grading period" do
course = Course.create!
grading_period_group = group_helper.legacy_create_for_course(course)
grading_period = grading_period_group.grading_periods.create!(
title: "unconnected to the user's course",
start_date: 2.months.ago,
end_date: 2.months.from_now(now)
)
@user_params[:grading_period_id] = grading_period.id
raw_api_call(:get, @user_path, @user_params)
expect(response).not_to be_ok
end
it "excludes soft-concluded courses when using state[]=invited or active" do
course0 = @course
course_with_student user: @student, enrollment_state: 'invited', active_course: true
json = api_call_as_user @student, :get, @user_path, @user_params.merge(state: %w(invited active))
expect(json.map { |e| e['course_id'] }).to match_array [course0.id, @course.id]
@course.start_at = 1.month.ago
@course.conclude_at = 1.week.ago
@course.restrict_enrollments_to_course_dates = true
@course.save!
json = api_call_as_user @student, :get, @user_path, @user_params.merge(state: %w(invited active))
expect(json.map { |e| e['course_id'] }).to match_array [course0.id]
end
describe "grade summary" do
let!(:grade_assignments) do
first = @course.assignments.create! due_at: 1.month.ago
last = @course.assignments.create! due_at: 1.month.from_now
no_due_at = @course.assignments.create!
Timecop.freeze(@first_grading_period.end_date - 1.day) do
first.grade_student @user, grade: 7, grader: @teacher
end
last.grade_student @user, grade: 10, grader: @teacher
no_due_at.grade_student @user, grade: 1, grader: @teacher
end
describe "provides a grade summary" do
it "for assignments due during the first grading period." do
@user_params[:grading_period_id] = @first_grading_period.id
raw_api_call(:get, @user_path, @user_params)
final_score = JSON.parse(response.body).first["grades"]["final_score"]
# ten times assignment's grade of 7
expect(final_score).to eq 70
end
it "for assignments due during the last grading period." do
@user_params[:grading_period_id] = @last_grading_period.id
raw_api_call(:get, @user_path, @user_params)
final_score = JSON.parse(response.body).first["grades"]["final_score"]
# ((10 + 1) / 1) * 10 => 110
# ((last + no_due_at) / number_of_grading_periods) * 10
expect(final_score).to eq 110
end
it "for all assignments when no grading period is specified." do
@user_params[:grading_period_id] = nil
raw_api_call(:get, @user_path, @user_params)
final_score = JSON.parse(response.body).first["grades"]["final_score"]
# ((7 + 10 + 1) / 2) * 10 => 60
# ((first + last + no_due_at) / number_of_grading_periods) * 10
expect(final_score).to eq 90
end
end
end
it "returns grades for the requested grading period for courses" do
Timecop.freeze(@first_grading_period.end_date - 1.day) do
@assignment_in_first_period.grade_student(@student, grade: 10, grader: @teacher)
end
@assignment_in_last_period.grade_student(@student, grade: 0, grader: @teacher)
student_grade = lambda do |json|
student_json = json.find { |e|
e["type"] == "StudentEnrollment"
}
if student_json
student_json["grades"]["final_score"]
end
end
json = api_call(:get, @path, @params)
expect(student_grade.(json)).to eq 50
@params[:grading_period_id] = @first_grading_period.id
json = api_call(:get, @path, @params)
expect(student_grade.(json)).to eq 100
@params[:grading_period_id] = @last_grading_period.id
json = api_call(:get, @path, @params)
expect(student_grade.(json)).to eq 0
end
end
end
context "an account admin" do
before :once do
@user = user_with_pseudonym(:username => 'admin@example.com')
Account.default.account_users.create!(user: @user)
end
it "is able to return an enrollment object by id" do
json = api_call(:get, "#{@enroll_path}/#{@enrollment.id}", @enroll_params)
expect(json).to eq({
'root_account_id' => @enrollment.root_account_id,
'id' => @enrollment.id,
'user_id' => @student.id,
'course_section_id' => @enrollment.course_section_id,
'sis_import_id' => @enrollment.sis_batch_id,
'sis_account_id' => nil,
'sis_course_id' => nil,
'sis_section_id' => nil,
'sis_user_id' => nil,
'course_integration_id' => nil,
'section_integration_id' => nil,
'limit_privileges_to_course_section' => @enrollment.limit_privileges_to_course_section,
'enrollment_state' => @enrollment.workflow_state,
'course_id' => @course.id,
'type' => @enrollment.type,
'role' => @enrollment.role.name,
'role_id' => @enrollment.role.id,
'html_url' => course_user_url(@course, @student),
'grades' => {
'html_url' => course_student_grades_url(@course, @student),
'final_score' => nil,
'current_score' => nil,
'final_grade' => nil,
'current_grade' => nil,
'unposted_final_score' => nil,
'unposted_current_score' => nil,
'unposted_final_grade' => nil,
'unposted_current_grade' => nil
},
'associated_user_id' => @enrollment.associated_user_id,
'updated_at' => @enrollment.updated_at.xmlschema,
'created_at' => @enrollment.created_at.xmlschema,
'start_at' => nil,
'end_at' => nil,
'last_activity_at' => nil,
'last_attended_at' => nil,
'total_activity_time' => 0
})
end
it "lists all of a user's enrollments in an account" do
e = @student.enrollments.current.first
sis_batch = e.root_account.sis_batches.create
SisBatch.where(id: sis_batch).update_all(workflow_state: 'imported')
e.sis_batch_id = sis_batch.id
e.save!
json = api_call(:get, @user_path, @user_params)
enrollments = @student.enrollments.current.eager_load(:user).order("users.sortable_name ASC")
expect(json).to eq enrollments.map { |e|
{
'root_account_id' => e.root_account_id,
'limit_privileges_to_course_section' => e.limit_privileges_to_course_section,
'enrollment_state' => e.workflow_state,
'id' => e.id,
'user_id' => e.user_id,
'type' => e.type,
'role' => e.role.name,
'role_id' => e.role.id,
'course_section_id' => e.course_section_id,
'course_id' => e.course_id,
'sis_import_id' => sis_batch.id,
'sis_account_id' => @course.account.sis_source_id,
'sis_course_id' => @course.sis_source_id,
'course_integration_id' => @course.integration_id,
'sis_section_id' => @section.sis_source_id,
'sis_user_id' => @student.pseudonym.sis_user_id,
'section_integration_id' => @section.integration_id,
'user' => {
'name' => e.user.name,
'sortable_name' => e.user.sortable_name,
'short_name' => e.user.short_name,
'sis_user_id' => nil,
'integration_id' => nil,
'sis_import_id' => nil,
'id' => e.user.id,
'created_at' => e.user.created_at.iso8601,
'login_id' => e.user.pseudonym ? e.user.pseudonym.unique_id : nil
},
'html_url' => course_user_url(e.course_id, e.user_id),
'grades' => {
'html_url' => course_student_grades_url(e.course_id, e.user_id),
'final_score' => nil,
'current_score' => nil,
'final_grade' => nil,
'current_grade' => nil,
'unposted_final_score' => nil,
'unposted_current_score' => nil,
'unposted_final_grade' => nil,
'unposted_current_grade' => nil
},
'associated_user_id' => nil,
'updated_at' => e.updated_at.xmlschema,
'created_at' => e.created_at.xmlschema,
'start_at' => nil,
'end_at' => nil,
'last_activity_at' => nil,
'last_attended_at' => nil,
'total_activity_time' => 0
}
}
end
context "filtering by SIS IDs" do
context "filtering by sis_account_id" do
before(:once) do
root_account_id = @course.account.id
@subaccount = Account.create!(parent_account_id: root_account_id)
@subaccount.root_account_id = root_account_id
@subaccount.sis_source_id = '1234'
@subaccount.save!
@course.update_attribute(:account_id, @subaccount.id)
end
it "filters by a single sis_account_id" do
@params[:sis_account_id] = '1234'
json = api_call(:get, @path, @params)
student_ids = json.map { |e| e['user_id'] }
expect(json.length).to eq(2)
expect(json.first['sis_account_id']).to eq(@subaccount.sis_source_id)
expect(student_ids).to match_array([@teacher.id, @student.id])
end
it "filters by a list of sis_account_ids" do
@params[:sis_account_id] = ['1234', '5678']
json = api_call(:get, @path, @params)
student_ids = json.map { |e| e['user_id'] }
expect(json.length).to eq(2)
expect(json.first['sis_account_id']).to eq(@subaccount.sis_source_id)
expect(student_ids).to match_array([@teacher.id, @student.id])
end
it "returns nothing if there are no matching sis_account_ids" do
@params[:sis_account_id] = '5678'
json = api_call(:get, @path, @params)
expect(json).to be_empty
end
end
context "filtering by sis_user_id" do
before :once do
@teacher.pseudonym.update_attribute(:sis_user_id, '1234')
end
it "filters by a single sis_user_id" do
@params[:sis_user_id] = '1234'
json = api_call(:get, @path, @params)
expect(json.length).to eq(1)
expect(json.first['sis_user_id']).to eq(@teacher.pseudonym.sis_user_id)
end
it "filters enrollments not made with sis_user_id" do
section = @course.course_sections.create!(name: 'other_section')
e = section.enroll_user(@teacher, 'TeacherEnrollment')
# generally these are populated from a sis_import
Enrollment.where(id: e).update_all(sis_pseudonym_id: @teacher.pseudonyms.where(sis_user_id: '1234').take.id)
@params[:sis_user_id] = '1234'
@params[:created_for_sis_id] = true
json = api_call(:get, @path, @params)
expect(json.length).to eq(1)
expect(json.first['id']).to eq(e.id)
end
it "filters by a list of sis_user_ids" do
@params[:sis_user_id] = ['1234', '5678']
json = api_call(:get, @path, @params)
expect(json.length).to eq(1)
expect(json.first['sis_user_id']).to eq(@teacher.pseudonym.sis_user_id)
end
it "returns nothing if there are no matching sis_user_ids" do
@params[:sis_user_id] = '5678'
json = api_call(:get, @path, @params)
expect(json).to be_empty
end
end
context "filtering by sis_section_id" do
before :once do
@course.course_sections.first.update_attribute(:sis_source_id, 'SIS123')
end
it "filters by a single sis_section_id" do
@params[:sis_section_id] = 'SIS123'
json = api_call(:get, @path, @params)
json_user_ids = json.map { |user| user["user_id"] }
section_user_ids = @course.course_sections.first.enrollments.map { |e| e.user_id }
expect(json.length).to eq (@course.course_sections.first.enrollments.length)
expect(json_user_ids).to match_array(section_user_ids)
end
it "filters by a list of sis_section_ids" do
@params[:sis_section_id] = ['SIS123', 'SIS456']
json = api_call(:get, @path, @params)
expect(json.length).to eq (@course.course_sections.first.enrollments.length)
json_user_ids = json.map { |user| user["user_id"] }
section_user_ids = @course.course_sections.first.enrollments.map { |e| e.user_id }
expect(json_user_ids).to match_array(section_user_ids)
end
it "returns nothing if there are no matching sis_section_ids" do
@params[:sis_section_id] = '5678'
json = api_call(:get, @path, @params)
expect(json).to be_empty
end
end
context "filtering by sis_course_id" do
before :once do
@course.update_attribute(:sis_source_id, 'SIS123')
end
it "filters by a single sis_course_id" do
@params[:sis_course_id] = 'SIS123'
json = api_call(:get, @path, @params)
expect(json.length).to eq (@course.enrollments.length)
json_user_ids = json.map { |user| user["user_id"] }
course_user_ids = @course.enrollments.map { |e| e.user_id }
expect(json_user_ids).to match_array(course_user_ids)
end
it "filters by a list of sis_course_ids" do
@params[:sis_course_id] = ['SIS123', 'LULZ']
json = api_call(:get, @path, @params)
expect(json.length).to eq (@course.enrollments.length)
json_user_ids = json.map { |user| user["user_id"] }
course_user_ids = @course.enrollments.map { |e| e.user_id }
expect(json_user_ids).to match_array(course_user_ids)
end
it "returns nothing if there are no matching sis_course_ids" do
@params[:sis_course_id] = 'NONONO'
json = api_call(:get, @path, @params)
expect(json).to be_empty
end
end
end
context 'group_ids' do
it "includes a users group_ids if group_ids are in include" do
@path = "/api/v1/courses/#{@course.id}/enrollments"
@params = { :controller => "enrollments_api", :action => "index", :course_id => @course.id.to_param, :format => "json", :include => ["group_ids"] }
enrollments_json = api_call(:get, @path, @params)
expect(enrollments_json[0]["user"]["group_ids"]).to eq([@group.id])
end
it "does not include a users deleted memberships" do
@group.group_memberships.update_all(:workflow_state => "deleted")
@path = "/api/v1/courses/#{@course.id}/enrollments"
@params = { :controller => "enrollments_api", :action => "index", :course_id => @course.id.to_param, :format => "json", :include => ["group_ids"] }
json = api_call(:get, @path, @params)
expect(json[0]["user"]["group_ids"]).to be_empty
end
it "does not include ids from different contexts" do
original_course = @course
course_factory(active_all: true, :user => @user)
group2 = @course.groups.create!(:name => "My Group")
group2.add_user(@student, 'accepted', true)
@course = original_course
@path = "/api/v1/courses/#{@course.id}/enrollments"
@params = { :controller => "enrollments_api", :action => "index", :course_id => @course.id.to_param, :format => "json", :include => ["group_ids"] }
enrollments_json = api_call(:get, @path, @params)
expect(enrollments_json[0]["user"]["group_ids"]).to include(@group.id)
expect(enrollments_json[0]["user"]["group_ids"]).not_to include(group2.id)
end
end
it "shows last_activity_at and total_activity_time for student enrollment" do
enrollment = @course.student_enrollments.first
recent_activity = Enrollment::RecentActivity.new(enrollment)
recent_activity.record!(Time.zone.now - 5.minutes)
recent_activity.record!(Time.zone.now)
json = api_call(:get, @user_path, @user_params)
enrollments = @student.enrollments.current.eager_load(:user).order("users.sortable_name ASC")
expect(json).to eq enrollments.map { |e|
{
'root_account_id' => e.root_account_id,
'limit_privileges_to_course_section' => e.limit_privileges_to_course_section,
'enrollment_state' => e.workflow_state,
'id' => e.id,
'user_id' => e.user_id,
'type' => e.type,
'role' => e.role.name,
'role_id' => e.role.id,
'course_section_id' => e.course_section_id,
'course_id' => e.course_id,
'sis_import_id' => nil,
'sis_account_id' => @course.account.sis_source_id,
'sis_course_id' => @course.sis_source_id,
'course_integration_id' => @course.integration_id,
'sis_section_id' => @section.sis_source_id,
'sis_user_id' => @student.pseudonym.sis_user_id,
'section_integration_id' => @section.integration_id,
'user' => {
'name' => e.user.name,
'sortable_name' => e.user.sortable_name,
'short_name' => e.user.short_name,
'sis_user_id' => e.user.pseudonym ? e.user.pseudonym&.sis_user_id : nil,
'integration_id' => e.user.pseudonym ? e.user.pseudonym&.integration_id : nil,
'sis_import_id' => e.user.pseudonym ? e.user.pseudonym.sis_batch_id : nil,
'id' => e.user.id,
'created_at' => e.user.created_at.iso8601,
'login_id' => e.user.pseudonym ? e.user.pseudonym.unique_id : nil
},
'html_url' => course_user_url(e.course_id, e.user_id),
'grades' => {
'html_url' => course_student_grades_url(e.course_id, e.user_id),
'final_score' => nil,
'current_score' => nil,
'final_grade' => nil,
'current_grade' => nil,
"unposted_current_score" => nil,
"unposted_current_grade" => nil,
"unposted_final_score" => nil,
"unposted_final_grade" => nil
},
'associated_user_id' => nil,
'updated_at' => e.updated_at.xmlschema,
'created_at' => e.created_at.xmlschema,
'start_at' => nil,
'end_at' => nil,
'last_activity_at' => e.last_activity_at.xmlschema,
'last_attended_at' => nil,
'total_activity_time' => e.total_activity_time
}
}
end
it "returns enrollments for unpublished courses" do
course_factory
@course.claim
enrollment = course_factory.enroll_student(@student)
enrollment.update_attribute(:workflow_state, 'active')
# without a state[] filter
json = api_call(:get, @user_path, @user_params)
expect(json.map { |e| e['id'] }).to include enrollment.id
# with a state[] filter
json = api_call(:get, "#{@user_path}?state[]=active",
@user_params.merge(:state => %w{active}))
expect(json.map { |e| e['id'] }).to include enrollment.id
end
it "does not return enrollments from other accounts" do
# enroll the user in a course in another account
account = Account.create!(:name => 'Account Two')
course = course_factory(:account => account, :course_name => 'Account Two Course', :active_course => true)
course.enroll_user(@student).accept!
json = api_call(:get, @user_path, @user_params)
expect(json.length).to eql 1
end
it "lists section enrollments properly" do
enrollment = @student.enrollments.first
enrollment.course_section = @section
enrollment.save!
@path = "/api/v1/sections/#{@section.id}/enrollments"
@params = { :controller => "enrollments_api", :action => "index", :section_id => @section.id.to_param, :format => "json" }
json = api_call(:get, @path, @params)
expect(json.length).to eql 1
expect(json.all? { |r| r["course_section_id"] == @section.id }).to be_truthy
end
it "lists deleted section enrollments properly" do
enrollment = @student.enrollments.first
enrollment.course_section = @section
enrollment.save!
enrollment.destroy
@path = "/api/v1/sections/#{@section.id}/enrollments?state[]=deleted"
@params = { :controller => "enrollments_api", :action => "index", :section_id => @section.id.to_param, :format => "json", :state => ["deleted"] }
json = api_call(:get, @path, @params)
expect(json.length).to eql 1
expect(json.all? { |r| r["course_section_id"] == @section.id }).to be_truthy
@path = "/api/v1/sections/#{@section.id}/enrollments"
@params = { controller: "enrollments_api", action: "index", section_id: @section.id.to_param, format: "json" }
json = api_call(:get, @path, @params)
expect(json.length).to be 0
end
it 'lists enrollments in deleted sections as deleted' do
enrollment = @student.enrollments.first
enrollment.course_section = @section
enrollment.save!
CourseSection.where(id: @section.id).update_all(workflow_state: 'deleted')
path = "/api/v1/users/#{@student.id}/enrollments"
params = { controller: 'enrollments_api', action: 'index', user_id: @student.id.to_param, format: 'json' }
json = api_call(:get, path, params)
expect(json.first['enrollment_state']).to eql 'deleted'
end
describe "custom roles" do
context "user context" do
before :once do
@original_course = @course
course_factory.offer!
@role = @course.account.roles.build :name => 'CustomStudent'
@role.base_role_type = 'StudentEnrollment'
@role.save!
@course.enroll_user(@student, 'StudentEnrollment', :role => @role)
end
it "includes derived roles when called with type=StudentEnrollment" do
json = api_call(:get, "#{@user_path}?type=StudentEnrollment", @user_params.merge(:type => 'StudentEnrollment'))
expect(json.map { |e| e['course_id'].to_i }.sort).to eq [@original_course.id, @course.id].sort
end
context "with role parameter" do
it "includes only vanilla StudentEnrollments when called with role=StudentEnrollment" do
json = api_call(:get, "#{@user_path}?role=StudentEnrollment", @user_params.merge(:role => 'StudentEnrollment'))
expect(json.map { |e| e['course_id'].to_i }).to eq [@original_course.id]
end
it "filters by custom role" do
json = api_call(:get, "#{@user_path}?role=CustomStudent", @user_params.merge(:role => 'CustomStudent'))
expect(json.map { |e| e['course_id'].to_i }).to eq [@course.id]
expect(json[0]['role']).to eq 'CustomStudent'
end
it "accepts an array of enrollment roles" do
json = api_call(:get, "#{@user_path}?role[]=StudentEnrollment&role[]=CustomStudent",
@user_params.merge(:role => %w{StudentEnrollment CustomStudent}))
expect(json.map { |e| e['course_id'].to_i }.sort).to eq [@original_course.id, @course.id].sort
end
end
context "with role_id parameter" do
it "includes only vanilla StudentEnrollments when called with built in role_id" do
json = api_call(:get, "#{@user_path}?role_id=#{student_role.id}", @user_params.merge(:role_id => student_role.id))
expect(json.map { |e| e['course_id'].to_i }).to eq [@original_course.id]
end
it "filters by custom role" do
json = api_call(:get, "#{@user_path}?role_id=#{@role.id}", @user_params.merge(:role_id => @role.id))
expect(json.map { |e| e['course_id'].to_i }).to eq [@course.id]
expect(json[0]['role']).to eq 'CustomStudent'
expect(json[0]['role_id']).to eq @role.id
end
it "accepts an array of enrollment roles" do
json = api_call(:get, "#{@user_path}?role_id[]=#{student_role.id}&role_id[]=#{@role.id}",
@user_params.merge(:role_id => [student_role.id, @role.id].map(&:to_param)))
expect(json.map { |e| e['course_id'].to_i }.sort).to eq [@original_course.id, @course.id].sort
end
end
end
context "course context" do
before :once do
role = @course.account.roles.build :name => 'CustomStudent'
role.base_role_type = 'StudentEnrollment'
role.save!
@original_student = @student
student_in_course(:course => @course, :role => role)
end
it "includes derived roles when called with type=StudentEnrollment" do
json = api_call(:get, "#{@path}?type=StudentEnrollment", @params.merge(:type => 'StudentEnrollment'))
expect(json.map { |e| e['user_id'].to_i }.sort).to eq [@original_student.id, @student.id].sort
end
it "includes only vanilla StudentEnrollments when called with role=StudentEnrollment" do
json = api_call(:get, "#{@path}?role=StudentEnrollment", @params.merge(:role => 'StudentEnrollment'))
expect(json.map { |e| e['user_id'].to_i }).to eq [@original_student.id]
end
it "filters by custom role" do
json = api_call(:get, "#{@path}?role=CustomStudent", @params.merge(:role => 'CustomStudent'))
expect(json.map { |e| e['user_id'].to_i }).to eq [@student.id]
expect(json[0]['role']).to eq 'CustomStudent'
end
it "accepts an array of enrollment roles" do
json = api_call(:get, "#{@path}?role[]=StudentEnrollment&role[]=CustomStudent",
@params.merge(:role => %w{StudentEnrollment CustomStudent}))
expect(json.map { |e| e['user_id'].to_i }.sort).to eq [@original_student.id, @student.id].sort
end
end
end
end
context "a student" do
it "lists all members of a course" do
current_user = @user
enrollment = @course.enroll_user(user_factory)
enrollment.accept!
@user = current_user
json = api_call(:get, @path, @params)
enrollments = %w{observer student ta teacher}.inject([]) do |res, type|
res + @course.send("#{type}_enrollments").eager_load(:user).order(User.sortable_name_order_by_clause("users"))
end
expect(json).to match_array enrollments.map { |e|
h = {
'root_account_id' => e.root_account_id,
'limit_privileges_to_course_section' => e.limit_privileges_to_course_section,
'enrollment_state' => e.workflow_state,
'id' => e.id,
'user_id' => e.user_id,
'type' => e.type,
'role' => e.role.name,
'role_id' => e.role.id,
'course_section_id' => e.course_section_id,
'course_id' => e.course_id,
'html_url' => course_user_url(@course, e.user),
'associated_user_id' => nil,
'updated_at' => e.updated_at.xmlschema,
'created_at' => e.created_at.xmlschema,
'start_at' => nil,
'end_at' => nil,
'user' => {
'name' => e.user.name,
'sortable_name' => e.user.sortable_name,
'short_name' => e.user.short_name,
'id' => e.user.id,
'created_at' => e.user.created_at.iso8601
}
}
# should display the user's own grades
h['grades'] = {
'html_url' => course_student_grades_url(@course, e.user),
'final_score' => nil,
'current_score' => nil,
'final_grade' => nil,
'current_grade' => nil,
} if e.student? && e.user_id == @user.id
# should not display grades for other users.
h['grades'] = {
'html_url' => course_student_grades_url(@course, e.user)
} if e.student? && e.user_id != @user.id
h.merge!(
'last_activity_at' => nil,
'last_attended_at' => nil,
'total_activity_time' => 0
) if e.user == @user
h
}
end
it "is able to return an enrollment object by id" do
json = api_call(:get, "#{@enroll_path}/#{@enrollment.id}", @enroll_params)
expect(json).to eq({
'root_account_id' => @enrollment.root_account_id,
'id' => @enrollment.id,
'user_id' => @student.id,
'course_section_id' => @enrollment.course_section_id,
'limit_privileges_to_course_section' => @enrollment.limit_privileges_to_course_section,
'enrollment_state' => @enrollment.workflow_state,
'course_id' => @course.id,
'type' => @enrollment.type,
'role' => @enrollment.role.name,
'role_id' => @enrollment.role.id,
'html_url' => course_user_url(@course, @student),
'grades' => {
'html_url' => course_student_grades_url(@course, @student),
'final_score' => nil,
'current_score' => nil,
'final_grade' => nil,
'current_grade' => nil,
},
'associated_user_id' => @enrollment.associated_user_id,
'updated_at' => @enrollment.updated_at.xmlschema,
'created_at' => @enrollment.created_at.xmlschema,
'start_at' => nil,
'end_at' => nil,
'last_activity_at' => nil,
'last_attended_at' => nil,
'total_activity_time' => 0
})
end
it "filters by enrollment workflow_state" do
@teacher.enrollments.first.update_attribute(:workflow_state, 'completed')
json = api_call(:get, "#{@path}?state[]=completed", @params.merge(:state => %w{completed}))
expect(json.count).to be > 0
json.each { |e| expect(e['enrollment_state']).to eql 'completed' }
end
it "lists its own enrollments" do
json = api_call(:get, @user_path, @user_params)
enrollments = @user.enrollments.current.eager_load(:user).order("users.sortable_name ASC")
expect(json).to eq enrollments.map { |e|
{
'root_account_id' => e.root_account_id,
'limit_privileges_to_course_section' => e.limit_privileges_to_course_section,
'enrollment_state' => e.workflow_state,
'id' => e.id,
'user_id' => e.user_id,
'type' => e.type,
'role' => e.role.name,
'role_id' => e.role.id,
'course_section_id' => e.course_section_id,
'course_id' => e.course_id,
'user' => {
'name' => e.user.name,
'sortable_name' => e.user.sortable_name,
'short_name' => e.user.short_name,
'id' => e.user.id,
'login_id' => @user.pseudonym.unique_id,
'created_at' => e.user.created_at.iso8601
},
'html_url' => course_user_url(e.course_id, e.user_id),
'grades' => {
'html_url' => course_student_grades_url(e.course_id, e.user_id),
'final_score' => nil,
'current_score' => nil,
'final_grade' => nil,
'current_grade' => nil,
},
'associated_user_id' => nil,
'updated_at' => e.updated_at.xmlschema,
'created_at' => e.created_at.xmlschema,
'start_at' => nil,
'end_at' => nil,
'last_activity_at' => nil,
'last_attended_at' => nil,
'total_activity_time' => 0
}
}
end
context "override scores" do
let(:student_grades) do
json = api_call(:get, @user_path, @user_params)
json.first.fetch("grades")
end
before(:once) do
@enrollment.scores.create!(course_score: true, current_score: 67, override_score: 81)
@course.enable_feature!(:final_grades_override)
@course.update!(allow_final_grade_override: true, grading_standard_enabled: true)
end
context "when Final Grade Override is enabled and allowed" do
it "sets current_score to the override score" do
expect(student_grades.fetch("current_score")).to be 81.0
end
it "sets current_grade to the override grade" do
expect(student_grades.fetch("current_grade")).to eq "B-"
end
end
context "when Final Grade Override is not allowed" do
before(:once) do
@course.update!(allow_final_grade_override: false)
end
it "sets current_score to the computed score" do
expect(student_grades.fetch("current_score")).to be 67.0
end
it "sets current_grade to the computed grade" do
expect(student_grades.fetch("current_grade")).to eq "D+"
end
end
context "when Final Grade Override is disabled" do
before(:once) do
@course.disable_feature!(:final_grades_override)
end
it "sets current_score to the computed score" do
expect(student_grades.fetch("current_score")).to be 67.0
end
it "sets current_grade to the computed grade" do
expect(student_grades.fetch("current_grade")).to eq "D+"
end
end
end
describe "current points" do
let_once(:course) { Course.create! }
let_once(:student) { User.create! }
let_once(:teacher) { User.create! }
let_once(:base_params) { { controller: "enrollments_api", action: "index", format: "json" } }
before(:once) do
course.offer!
course.enroll_teacher(teacher, enrollment_state: "active")
enrollment = course.enroll_student(student, enrollment_state: "active")
enrollment.scores.create!(current_points: 75, unposted_current_points: 99)
end
context "for a user who can manage grades for the enrollment's course" do
let_once(:api_path) { "/api/v1/courses/#{course.id}/enrollments" }
let_once(:params_without_points) { base_params.merge({ course_id: course.id.to_param }) }
context "when requesting current points" do
let(:enrollment_grades_json) do
params = params_without_points.merge({ include: ["current_points"] })
json = api_call_as_user(teacher, :get, api_path, params)
json.find { |enrollment| enrollment["user_id"] == student.id }["grades"]
end
it "includes the current_points field" do
expect(enrollment_grades_json["current_points"]).to eq 75
end
it "includes the unposted_current_points field" do
expect(enrollment_grades_json["unposted_current_points"]).to eq 99
end
end
context "when not requesting current points" do
let(:enrollment_grades_json) do
json = api_call_as_user(teacher, :get, api_path, params_without_points)
json.find { |enrollment| enrollment["user_id"] == student.id }["grades"]
end
it "does not include the current_points field" do
expect(enrollment_grades_json).not_to include("current_points")
end
it "does not include the unposted_current_points field" do
expect(enrollment_grades_json).not_to include("unposted_current_points")
end
end
end
context "for a student viewing their own enrollment" do
let_once(:api_path) { "/api/v1/users/#{student.id}/enrollments" }
let_once(:params_without_points) { base_params.merge({ user_id: student.id.to_param }) }
context "when requesting current points" do
let(:enrollment_grades_json) do
params = params_without_points.merge({ include: ["current_points"] })
json = api_call_as_user(student, :get, api_path, params)
json.find { |enrollment| enrollment["user_id"] == student.id }["grades"]
end
it "includes the current_points field" do
expect(enrollment_grades_json["current_points"]).to eq 75
end
it "does not include the unposted_current_points field" do
expect(enrollment_grades_json).not_to include("unposted_current_points")
end
end
context "when not requesting current points" do
let(:enrollment_grades_json) do
json = api_call_as_user(student, :get, api_path, params_without_points)
json.find { |enrollment| enrollment["user_id"] == student.id }["grades"]
end
it "does not return the current_points field" do
expect(enrollment_grades_json).not_to include("current_points")
end
it "does not return the unposted_current_points field" do
expect(enrollment_grades_json).not_to include("unposted_current_points")
end
end
end
end
it "does not display grades when hide_final_grades is true for the course" do
@course.hide_final_grades = true
@course.save
json = api_call(:get, @user_path, @user_params)
expect(json[0]['grades'].keys).to eql %w{html_url}
end
it "does not show enrollments for courses that aren't published" do
course_factory
@course.claim
enrollment = course_factory.enroll_student(@user)
enrollment.update_attribute(:workflow_state, 'active')
# Request w/o a state[] filter.
json = api_call(:get, @user_path, @user_params)
expect(json.map { |e| e['id'] }).not_to include enrollment.id
# Request w/ a state[] filter.
json = api_call(:get, @user_path,
@user_params.merge(:state => %w{active}, :type => %w{StudentEnrollment}))
expect(json.map { |e| e['id'] }).not_to include enrollment.id
end
it "shows enrollments for courses that aren't published if state[]=current_and_future" do
course_factory
@course.claim
enrollment = @course.enroll_student(@user)
enrollment.update_attribute(:workflow_state, 'active')
json = api_call(:get, @user_path,
@user_params.merge(:state => %w{current_and_future}, :type => %w{StudentEnrollment}))
expect(json.map { |e| e['id'] }).to include enrollment.id
end
it "shows enrollments for courses with future start dates if state[]=current_and_future" do
course_factory
@course.update(:start_at => 1.week.from_now, :restrict_enrollments_to_course_dates => true)
enrollment = @course.enroll_student(@user)
enrollment.update_attribute(:workflow_state, 'active')
expect(enrollment.enrollment_state.state).to eq "pending_active"
json = api_call(:get, @user_path,
@user_params.merge(:state => %w{current_and_future}, :type => %w{StudentEnrollment}))
expect(json.map { |e| e['id'] }).to include enrollment.id
end
it "accepts multiple state[] filters" do
course_factory
@course.offer!
enrollment = course_factory.enroll_student(@user)
enrollment.update_attribute(:workflow_state, 'completed')
json = api_call(:get, @user_path,
@user_params.merge(:state => %w{active completed}))
expect(json.map { |e| e['id'].to_i }.sort).to eq @user.enrollments.map(&:id).sort
end
it "excludes invited enrollments in soft-concluded courses" do
term = Account.default.enrollment_terms.create! :end_at => 1.day.ago
enrollment1 = course_with_student :enrollment_state => :invited
enrollment1.course.offer!
enrollment1.course.enrollment_term = term
enrollment1.course.save!
enrollment2 = course_with_student :enrollment_state => :invited, :user => @student
enrollment2.course.offer!
json = api_call(:get, "/api/v1/users/self/enrollments", @user_params.merge(:user_id => 'self'))
expect(json.map { |el| el['id'] }).to match_array([enrollment2.id])
end
it "does not include the users' sis and login ids" do
json = api_call(:get, @path, @params)
json.each do |res|
%w{sis_user_id login_id}.each { |key| expect(res['user']).not_to include(key) }
end
end
end
context "a teacher" do
before do
@user = @teacher
end
it "includes users' sis and login ids" do
json = api_call(:get, @path, @params)
enrollments = %w{observer student ta teacher}.inject([]) do |res, type|
res + @course.send("#{type}_enrollments").preload(:user)
end
enrollments = enrollments.sort_by { |e| [e.type, e.user.sortable_name] }
expect(json).to eq(enrollments.map do |e|
user_json = {
'name' => e.user.name,
'sortable_name' => e.user.sortable_name,
'short_name' => e.user.short_name,
'id' => e.user.id,
'created_at' => e.user.created_at.iso8601,
'login_id' => e.user.pseudonym ? e.user.pseudonym.unique_id : nil
}
user_json.merge!({
'sis_user_id' => e.user.pseudonym.sis_user_id,
'integration_id' => e.user.pseudonym.integration_id,
})
h = {
'root_account_id' => e.root_account_id,
'limit_privileges_to_course_section' => e.limit_privileges_to_course_section,
'enrollment_state' => e.workflow_state,
'id' => e.id,
'user_id' => e.user_id,
'type' => e.type,
'role' => e.role.name,
'role_id' => e.role.id,
'course_section_id' => e.course_section_id,
'course_id' => e.course_id,
'user' => user_json,
'html_url' => course_user_url(@course, e.user),
'associated_user_id' => nil,
'updated_at' => e.updated_at.xmlschema,
'created_at' => e.created_at.xmlschema,
'start_at' => nil,
'end_at' => nil,
'last_activity_at' => nil,
'last_attended_at' => nil,
'total_activity_time' => 0,
'course_integration_id' => nil,
'sis_account_id' => nil,
'sis_course_id' => nil,
'sis_section_id' => nil,
'sis_user_id' => nil,
'section_integration_id' => nil
}
h['grades'] = {
'html_url' => course_student_grades_url(@course, e.user),
'final_score' => nil,
'current_score' => nil,
'final_grade' => nil,
'current_grade' => nil,
"unposted_current_score" => nil,
"unposted_current_grade" => nil,
"unposted_final_score" => nil,
"unposted_final_grade" => nil
} if e.student?
h
end)
end
context "override scores" do
let(:student_grades) do
json = api_call(:get, @path, @params)
json.detect { |enrollment| enrollment.fetch("id") == @enrollment.id }.fetch("grades")
end
before(:once) do
@course.enable_feature!(:final_grades_override)
@course.update!(allow_final_grade_override: true, grading_standard_enabled: true)
@enrollment.scores.create!(course_score: true, current_score: 67, override_score: 81)
end
context "when Final Grade Override is enabled and allowed" do
it "includes the override score" do
expect(student_grades.fetch("override_score")).to be 81.0
end
it "includes the override grade" do
expect(student_grades.fetch("override_grade")).to eq "B-"
end
it "continues to include the original score as current_score" do
expect(student_grades.fetch("current_score")).to be 67.0
end
it "continues to include the original grade as current_grade" do
expect(student_grades.fetch("current_grade")).to eq "D+"
end
it "excludes the override score when no override exists" do
@enrollment.scores.each(&:destroy!)
expect(student_grades).not_to have_key("override_score")
end
it "excludes the override grade when no override exists" do
@enrollment.scores.each(&:destroy!)
expect(student_grades).not_to have_key("override_grade")
end
end
context "when Final Grade Override is not allowed" do
before(:once) do
@course.update!(allow_final_grade_override: false)
end
it "excludes the override score" do
expect(student_grades).not_to have_key("override_score")
end
it "excludes the override grade" do
expect(student_grades).not_to have_key("override_grade")
end
end
context "when Final Grade Override is disabled" do
before(:once) do
@course.disable_feature!(:final_grades_override)
end
it "excludes the override score" do
expect(student_grades).not_to have_key("override_score")
end
it "excludes the override grade" do
expect(student_grades).not_to have_key("override_grade")
end
end
end
end
context "as an observer in a course" do
let(:course) { Course.create! }
let(:observed_student) { User.create! }
let(:hidden_student) { User.create! }
let(:observer) { User.create! }
let(:request_params) do
{
action: "index",
controller: "enrollments_api",
course_id: course.id,
format: :json
}
end
let(:enrollment_json) do
api_call_as_user(observer, :get, "/api/v1/courses/#{course.id}/enrollments", request_params)
end
let(:student_enrollments) do
enrollment_json.select { |enrollment| enrollment["type"] == "StudentEnrollment" }
end
let(:observer_enrollments) do
enrollment_json.select { |enrollment| enrollment["type"] == "ObserverEnrollment" }
end
before(:each) do
course.enroll_student(observed_student, active_all: true)
course.enroll_student(hidden_student, active_all: true)
observer.register!
# add an observer, but don't link them to any students yet
course.enroll_user(observer, 'ObserverEnrollment')
user_session(observer)
end
context "when the observer is observing at least one student in the course" do
before(:each) do
course.enroll_user(observer, 'ObserverEnrollment', associated_user_id: observed_student.id)
end
it "returns a successful response" do
api_call_as_user(observer, :get, "/api/v1/courses/#{course.id}/enrollments", request_params)
expect(response.code).to eq "200"
end
it "includes active enrollments for each observed student" do
expect(student_enrollments.pluck("user_id")).to contain_exactly(observed_student.id)
end
it "includes both the observer's base enrollment and enrollments associated with observees" do
expect(observer_enrollments.pluck("user_id", "associated_user_id")).to match_array([
[observer.id, nil],
[observer.id, observed_student.id]
])
end
it "does not include enrollments for students the user is not observing" do
expect(student_enrollments.pluck("user_id")).not_to include(hidden_student.id)
end
it "does not include students who were once observed but no longer are" do
observer.observer_enrollments.find_by(associated_user_id: observed_student.id).destroy
aggregate_failures do
expect(student_enrollments).to be_empty
expect(observer_enrollments.length).to eq 1
expect(observer_enrollments.first["associated_user_id"]).to be nil
end
end
it "returns unauthorized if the user has no non-deleted observer enrollments" do
observer.observer_enrollments.destroy_all
api_call_as_user(observer, :get, "/api/v1/courses/#{course.id}/enrollments", request_params)
expect(response.code).to eq "401"
end
end
it "returns only the base ObserverEnrollment if the observer has not been linked to any students" do
aggregate_failures do
expect(enrollment_json.length).to eq 1
expect(enrollment_json.first["user_id"]).to be observer.id
expect(enrollment_json.first["associated_user_id"]).to be nil
end
end
end
context "a user without permissions" do
before :once do
@user = user_with_pseudonym(:name => 'Don Draper', :username => 'ddraper@sterling-cooper.com')
end
it "returns 401 unauthorized for a course listing" do
raw_api_call(:get, "/api/v1/courses/#{@course.id}/enrollments", @params.merge(:course_id => @course.id.to_param))
expect(response.code).to eql "401"
end
it "returns 401 unauthorized for a user listing" do
raw_api_call(:get, @user_path, @user_params)
expect(response.code).to eql "401"
end
it "returns 401 unauthorized for a user requesting an enrollment object by id" do
raw_api_call(:get, "#{@enroll_path}/#{@enrollment.id}", @enroll_params)
expect(response.code).to eql '401'
end
it "returns 404 for a user querying from the wrong account" do
sub = @enrollment.root_account.sub_accounts.create!(name: "sub")
bad_path = "/api/v1/accounts/#{sub.id}/enrollments/#{@enrollment.id}"
enroll_params = {
:controller => "enrollments_api",
:action => "show",
:account_id => sub.id,
:id => @enrollment.id,
:format => "json"
}
raw_api_call(:get, bad_path, enroll_params)
expect(response.code).to eql '404'
end
end
context "a parent observer using parent app" do
before :once do
@student = user_factory(active_all: true, active_state: 'active')
3.times do
course_factory
@course.enroll_student(@student, enrollment_state: 'active')
end
@observer = user_factory(active_all: true, active_state: 'active')
add_linked_observer(@student, @observer)
@user = @observer
@user_path = "/api/v1/users/#{@student.id}/enrollments"
@user_params = { :controller => "enrollments_api", :action => "index", :user_id => @student.id.to_param, :format => "json" }
end
it "shows all enrollments for the observee (student)" do
json = api_call(:get, @user_path, @user_params)
expect(json.length).to eql 3
end
it "does not authorize the parent to see other students' enrollments" do
@other_student = user_factory(active_all: true, active_state: 'active')
@user = @observer
path = "/api/v1/users/#{@other_student.id}/enrollments"
params = { :controller => "enrollments_api", :action => "index", :user_id => @other_student.id.to_param, :format => "json" }
raw_api_call(:get, path, params)
expect(response.code).to eql '401'
end
end
describe "sharding" do
specs_require_sharding
context "when not scoped by a user" do
it "returns enrollments from the course's shard" do
@shard1.activate { @user = user_factory(active_user: true) }
account_admin_user(account: @course.account, user: @user)
json = api_call(:get, @path, @params)
enrollment_ids = json.collect { |e| e['id'] }
expect(enrollment_ids.sort).to eq(@course.enrollments.map(&:id).sort)
expect(json.length).to eq 2
end
end
context "when scoped by a user" do
it "returns enrollments from all of the current user's associated shards" do
# create a user on a different shard
@shard1.activate { @user = User.create!(name: 'outofshard') }
@course.enroll_student(@user)
# query own enrollment(s) as the out-of-shard user
@path = "#{@path}?user_id=self"
@params[:user_id] = 'self'
json = api_call(:get, @path, @params)
expect(json.length).to eq 1
expect(json.first['course_id']).to eq(@course.id)
expect(json.first['user_id']).to eq(@user.global_id)
end
it "returns enrollments from all of another user's associated shards" do
@shard1.activate { @other_course = Course.create! account: Account.create! }
@course.enroll_student(@user, enrollment_state: 'active')
@other_course.enroll_student(@user, enrollment_state: 'active')
@student = @user
@observer = user_factory
add_linked_observer(@student, @observer)
json = api_call_as_user(@observer, :get, "/api/v1/users/#{@student.id}/enrollments",
{ :controller => "enrollments_api", :action => "index", :user_id => @student.to_param,
:format => "json" })
courses = json.map { |el| el['course_id'] }
expect(courses).to include @course.id
expect(courses).to include @other_course.id
end
end
end
describe "pagination" do
shared_examples_for 'numeric pagination' do
it "properly paginates" do
json = api_call(:get, "#{@path}?page=1&per_page=1", @params.merge(:page => 1.to_param, :per_page => 1.to_param))
enrollments = %w{observer student ta teacher}.inject([]) { |res, type|
res = res + @course.send("#{type}_enrollments").preload(:user)
}.map do |e|
h = {
'root_account_id' => e.root_account_id,
'limit_privileges_to_course_section' => e.limit_privileges_to_course_section,
'enrollment_state' => e.workflow_state,
'id' => e.id,
'user_id' => e.user_id,
'type' => e.type,
'role' => e.role.name,
'role_id' => e.role.id,
'course_section_id' => e.course_section_id,
'course_id' => e.course_id,
'user' => {
'name' => e.user.name,
'sortable_name' => e.user.sortable_name,
'short_name' => e.user.short_name,
'id' => e.user.id,
'created_at' => e.user.created_at.iso8601
},
'html_url' => course_user_url(@course, e.user),
'associated_user_id' => nil,
'updated_at' => e.updated_at.xmlschema,
'created_at' => e.created_at.xmlschema,
'start_at' => nil,
'end_at' => nil,
}
h['grades'] = {
'html_url' => course_student_grades_url(@course, e.user),
'final_score' => nil,
'current_score' => nil,
'final_grade' => nil,
'current_grade' => nil,
} if e.student?
h.merge!(
'last_activity_at' => nil,
'last_attended_at' => nil,
'total_activity_time' => 0
) if e.user == @user
h
end
link_header = response.headers['Link'].split(',')
expect(link_header[0]).to match /page=1&per_page=1/ # current page
expect(link_header[1]).to match /page=2&per_page=1/ # next page
expect(link_header[2]).to match /page=1&per_page=1/ # first page
expect(link_header[3]).to match /page=2&per_page=1/ # last page
expect(json).to eql [enrollments[0]]
json = api_call(:get, "#{@path}?page=2&per_page=1", @params.merge(:page => 2.to_param, :per_page => 1.to_param))
link_header = response.headers['Link'].split(',')
expect(link_header[0]).to match /page=2&per_page=1/ # current page
expect(link_header[1]).to match /page=1&per_page=1/ # prev page
expect(link_header[2]).to match /page=1&per_page=1/ # first page
expect(link_header[3]).to match /page=2&per_page=1/ # last page
expect(json).to eql [enrollments[1]]
end
end
shared_examples_for 'bookmarked pagination' do
it "properly paginates" do
json = api_call(:get, "#{@path}?page=1&per_page=1", @params.merge(:page => 1.to_param, :per_page => 1.to_param))
enrollments = %w{observer student ta teacher}.inject([]) { |res, type|
res = res + @course.send("#{type}_enrollments").preload(:user)
}.map do |e|
h = {
'root_account_id' => e.root_account_id,
'limit_privileges_to_course_section' => e.limit_privileges_to_course_section,
'enrollment_state' => e.workflow_state,
'id' => e.id,
'user_id' => e.user_id,
'type' => e.type,
'role' => e.role.name,
'role_id' => e.role.id,
'course_section_id' => e.course_section_id,
'course_id' => e.course_id,
'user' => {
'name' => e.user.name,
'sortable_name' => e.user.sortable_name,
'short_name' => e.user.short_name,
'id' => e.user.id,
'created_at' => e.user.created_at.iso8601
},
'html_url' => course_user_url(@course, e.user),
'associated_user_id' => nil,
'updated_at' => e.updated_at.xmlschema,
'created_at' => e.created_at.xmlschema,
'start_at' => nil,
'end_at' => nil,
}
h['grades'] = {
'html_url' => course_student_grades_url(@course, e.user),
'final_score' => nil,
'current_score' => nil,
'final_grade' => nil,
'current_grade' => nil,
} if e.student?
h.merge!(
'last_activity_at' => nil,
'last_attended_at' => nil,
'total_activity_time' => 0
) if e.user == @user
h
end
link_header = response.headers['Link'].split(',')
expect(link_header[0]).to match /page=.*&per_page=1/ # current page
md = link_header[1].match(/page=(.*)&per_page=1/) # next page
bookmark = md[1]
expect(bookmark).to be_present
expect(link_header[2]).to match /page=.*&per_page=1/ # first page
expect(json).to eql [enrollments[0]]
json = api_call(:get, "#{@path}?page=#{bookmark}&per_page=1", @params.merge(:page => bookmark, :per_page => 1.to_param))
link_header = response.headers['Link'].split(',')
expect(link_header[0]).to match /page=#{bookmark}&per_page=1/ # current page
expect(link_header[1]).to match /page=.*&per_page=1/ # first page
expect(link_header[2]).to match /page=.*&per_page=1/ # last page
expect(json).to eql [enrollments[1]]
end
end
context 'with normal settings' do
it_behaves_like 'bookmarked pagination'
context 'with developer key pagination override' do
before do
global_id = Shard.global_id_for(DeveloperKey.default.id)
Setting.set("pagination_override_key_list", global_id.to_s)
end
it_behaves_like 'numeric pagination'
end
end
end
context "inactive enrollments" do
before do
@inactive_user = user_with_pseudonym(:name => "Inactive User")
student_in_course(:course => @course, :user => @inactive_user)
@inactive_enroll = @inactive_user.enrollments.first
@inactive_enroll.deactivate
end
it "excludes users with inactive enrollments for students" do
student_in_course(:course => @course, :active_all => true, :user => user_with_pseudonym)
json = api_call(:get, @path, @params)
expect(json.map { |e| e["id"] }).not_to include(@inactive_enroll.id)
end
it "includes users with inactive enrollments for teachers" do
teacher_in_course(:course => @course, :active_all => true, :user => user_with_pseudonym)
json = api_call(:get, @path, @params)
expect(json.map { |e| e["id"] }).to include(@inactive_enroll.id)
enroll_json = json.detect { |e| e["id"] == @inactive_enroll.id }
expect(enroll_json['user_id']).to eq @inactive_user.id
expect(enroll_json['enrollment_state']).to eq 'inactive'
end
end
describe "enrollment deletion, conclusion and inactivation" do
before :once do
course_with_student(:active_all => true, :user => user_with_pseudonym)
@enrollment = @student.enrollments.first
@teacher = User.create!(:name => 'Test Teacher')
@teacher.pseudonyms.create!(:unique_id => 'test+teacher@example.com')
@course.enroll_teacher(@teacher)
@user = @teacher
@path = "/api/v1/courses/#{@course.id}/enrollments/#{@enrollment.id}"
@params = { :controller => 'enrollments_api', :action => 'destroy', :course_id => @course.id.to_param,
:id => @enrollment.id.to_param, :format => 'json' }
end
before :each do
time = Time.now
allow(Time).to receive(:now).and_return(time)
end
context "an authorized user" do
it "is able to conclude an enrollment" do
json = api_call(:delete, "#{@path}?task=conclude", @params.merge(:task => 'conclude'))
@enrollment.reload
expect(json).to eq({
'root_account_id' => @enrollment.root_account_id,
'id' => @enrollment.id,
'user_id' => @student.id,
'course_section_id' => @enrollment.course_section_id,
'limit_privileges_to_course_section' => @enrollment.limit_privileges_to_course_section,
'enrollment_state' => 'completed',
'course_id' => @course.id,
'type' => @enrollment.type,
'role' => @enrollment.role.name,
'role_id' => @enrollment.role.id,
'html_url' => course_user_url(@course, @student),
'grades' => {
'html_url' => course_student_grades_url(@course, @student),
'final_score' => nil,
'current_score' => nil,
'final_grade' => nil,
'current_grade' => nil,
"unposted_current_score" => nil,
"unposted_current_grade" => nil,
"unposted_final_score" => nil,
"unposted_final_grade" => nil
},
'associated_user_id' => @enrollment.associated_user_id,
'updated_at' => @enrollment.updated_at.xmlschema,
'created_at' => @enrollment.created_at.xmlschema,
'start_at' => nil,
'end_at' => nil,
'last_activity_at' => nil,
'last_attended_at' => nil,
'total_activity_time' => 0,
'course_integration_id' => nil,
'sis_account_id' => nil,
'sis_course_id' => nil,
'sis_section_id' => nil,
'sis_user_id' => nil,
'section_integration_id' => nil
})
end
it "is not able to delete an enrollment for other courses" do
@account = Account.default
@sub_account = Account.create(:parent_account => @account, :name => 'English')
@sub_account.save!
@user = user_with_pseudonym(:username => 'sub_admin@example.com')
@sub_account.account_users.create!(user: @user)
@course = @sub_account.courses.create(name: 'sub')
@course.account_id = @sub_account.id
@course.save!
@path = "/api/v1/courses/#{@course.id}/enrollments/#{@enrollment.id}"
@params = { :controller => 'enrollments_api', :action => 'destroy', :course_id => @course.id.to_param,
:id => @enrollment.id.to_param, :format => 'json' }
raw_api_call(:delete, "#{@path}?task=delete", @params.merge(:task => 'delete'))
expect(response.code).to eql '404'
expect(JSON.parse(response.body)['errors']).to eq [{ 'message' => 'The specified resource does not exist.' }]
end
it "is able to delete an enrollment" do
json = api_call(:delete, "#{@path}?task=delete", @params.merge(:task => 'delete'))
@enrollment.reload
expect(json).to eq({
'root_account_id' => @enrollment.root_account_id,
'id' => @enrollment.id,
'user_id' => @student.id,
'course_section_id' => @enrollment.course_section_id,
'limit_privileges_to_course_section' => @enrollment.limit_privileges_to_course_section,
'enrollment_state' => 'deleted',
'course_id' => @course.id,
'type' => @enrollment.type,
'role' => @enrollment.role.name,
'role_id' => @enrollment.role.id,
'html_url' => course_user_url(@course, @student),
'grades' => {
'html_url' => course_student_grades_url(@course, @student),
'final_score' => nil,
'current_score' => nil,
'final_grade' => nil,
'current_grade' => nil,
"unposted_current_score" => nil,
"unposted_current_grade" => nil,
"unposted_final_score" => nil,
"unposted_final_grade" => nil
},
'associated_user_id' => @enrollment.associated_user_id,
'updated_at' => @enrollment.updated_at.xmlschema,
'created_at' => @enrollment.created_at.xmlschema,
'start_at' => nil,
'end_at' => nil,
'last_activity_at' => nil,
'last_attended_at' => nil,
'total_activity_time' => 0,
'course_integration_id' => nil,
'sis_account_id' => nil,
'sis_course_id' => nil,
'sis_section_id' => nil,
'sis_user_id' => nil,
'section_integration_id' => nil
})
end
it "is not able to unenroll itself if it can't re-enroll itself" do
enrollment = @teacher.enrollments.first
@path.sub!(@enrollment.id.to_s, enrollment.id.to_s)
@params.merge!(:id => enrollment.id.to_param, :task => 'delete')
raw_api_call(:delete, "#{@path}?task=delete", @params)
expect(response.code).to eql '401'
expect(JSON.parse(response.body)).to eq({
'errors' => [{ 'message' => 'user not authorized to perform that action' }],
'status' => 'unauthorized'
})
end
it "is able to deactivate an enrollment using the 'inactivate' task" do
json = api_call(:delete, "#{@path}?task=inactivate", @params.merge(:task => 'inactivate'))
expect(json['enrollment_state']).to eq 'inactive'
@enrollment.reload
expect(@enrollment.workflow_state).to eq 'inactive'
end
it "is able to deactivate an enrollment using the 'deactivate' task" do
json = api_call(:delete, "#{@path}?task=deactivate", @params.merge(:task => 'deactivate'))
expect(json['enrollment_state']).to eq 'inactive'
@enrollment.reload
expect(@enrollment.workflow_state).to eq 'inactive'
end
end
context "an unauthorized user" do
it "returns 401" do
@user = @student
raw_api_call(:delete, @path, @params)
expect(response.code).to eql '401'
raw_api_call(:delete, "#{@path}?task=delete", @params.merge(:task => 'delete'))
expect(response.code).to eql '401'
raw_api_call(:delete, "#{@path}?task=inactivate", @params.merge(:task => 'inactivate'))
expect(response.code).to eql '401'
raw_api_call(:delete, "#{@path}?task=deactivate", @params.merge(:task => 'deactivate'))
expect(response.code).to eql '401'
end
end
end
describe "enrollment reactivation" do
before :once do
course_with_student(:active_all => true, :user => user_with_pseudonym)
teacher_in_course(:course => @course, :user => user_with_pseudonym)
@enrollment = @student.enrollments.first
@enrollment.deactivate
@path = "/api/v1/courses/#{@course.id}/enrollments/#{@enrollment.id}/reactivate"
@params = { :controller => 'enrollments_api', :action => 'reactivate', :course_id => @course.id.to_param,
:id => @enrollment.id.to_param, :format => 'json' }
end
it "requires authorization" do
@user = @student
raw_api_call(:put, @path, @params)
expect(response.code).to eql '401'
end
it "is able to reactivate an enrollment" do
json = api_call(:put, @path, @params)
expect(json['enrollment_state']).to eq 'active'
@enrollment.reload
expect(@enrollment.workflow_state).to eq 'active'
end
end
describe "show" do
before(:once) do
@account = Account.default
account_admin_user(account: @account)
student_in_course active_all: true
@base_path = "/api/v1/accounts/#{@account.id}/enrollments"
@params = { :controller => 'enrollments_api', :action => 'show', :account_id => @account.to_param,
:format => 'json' }
end
context "admin" do
before(:once) do
@user = @admin
end
it "shows other's enrollment" do
json = api_call(:get, @base_path + "/#{@enrollment.id}", @params.merge(id: @enrollment.to_param))
expect(json['id']).to eql(@enrollment.id)
end
end
context "student" do
before(:once) do
@user = @student
end
it "shows own enrollment" do
json = api_call(:get, @base_path + "/#{@enrollment.id}", @params.merge(id: @enrollment.to_param))
expect(json['id']).to eql(@enrollment.id)
end
it "does not show other's enrollment" do
student = @student
other_enrollment = student_in_course(active_all: true)
@user = student
api_call(:get, @base_path + "/#{other_enrollment.id}", @params.merge(id: other_enrollment.to_param), {}, {}, { expected_status: 401 })
end
end
context "no user" do
before(:once) do
@user = nil
end
it "does not show enrollment" do
json = api_call(:get, @base_path + "/#{@enrollment.id}", @params.merge(id: @enrollment.to_param), {}, {}, { expected_status: 401 })
end
end
end
describe "filters" do
it "properly filters by a single enrollment type" do
json = api_call(:get, "#{@path}?type[]=StudentEnrollment", @params.merge(:type => %w{StudentEnrollment}))
expect(json).to eql @course.student_enrollments.map { |e|
{
'root_account_id' => e.root_account_id,
'limit_privileges_to_course_section' => e.limit_privileges_to_course_section,
'enrollment_state' => e.workflow_state,
'id' => e.id,
'user_id' => e.user_id,
'type' => e.type,
'role' => e.role.name,
'role_id' => e.role.id,
'course_section_id' => e.course_section_id,
'course_id' => e.course_id,
'html_url' => course_user_url(@course, e.user),
'grades' => {
'html_url' => course_student_grades_url(@course, e.user),
'final_score' => nil,
'current_score' => nil,
'final_grade' => nil,
'current_grade' => nil,
},
'associated_user_id' => nil,
'updated_at' => e.updated_at.xmlschema,
'created_at' => e.created_at.xmlschema,
'start_at' => nil,
'end_at' => nil,
'last_activity_at' => nil,
'last_attended_at' => nil,
'total_activity_time' => 0,
'user' => {
'name' => e.user.name,
'sortable_name' => e.user.sortable_name,
'short_name' => e.user.short_name,
'id' => e.user.id,
'created_at' => e.user.created_at.iso8601
}
}
}
end
it "properly filters by multiple enrollment types" do
# set up some enrollments that shouldn't be returned by the api
request_user = @user
@new_user = user_with_pseudonym(:name => 'Zombo', :username => 'nobody2@example.com')
@course.enroll_user(@new_user, 'TaEnrollment', :enrollment_state => 'active')
@course.enroll_user(@new_user, 'ObserverEnrollment', :enrollment_state => 'active')
@user = request_user
json = api_call(:get, "#{@path}?type[]=StudentEnrollment&type[]=TeacherEnrollment", @params.merge(:type => %w{StudentEnrollment TeacherEnrollment}))
enrollments = (@course.student_enrollments + @course.teacher_enrollments).sort_by { |e| [e.type, e.user.sortable_name] }
expect(json).to eq(enrollments.map { |e|
h = {
'root_account_id' => e.root_account_id,
'limit_privileges_to_course_section' => e.limit_privileges_to_course_section,
'enrollment_state' => e.workflow_state,
'id' => e.id,
'user_id' => e.user_id,
'type' => e.type,
'role' => e.role.name,
'role_id' => e.role.id,
'course_section_id' => e.course_section_id,
'course_id' => e.course_id,
'html_url' => course_user_url(@course, e.user),
'associated_user_id' => nil,
'updated_at' => e.updated_at.xmlschema,
'created_at' => e.created_at.xmlschema,
'start_at' => nil,
'end_at' => nil,
'user' => {
'name' => e.user.name,
'sortable_name' => e.user.sortable_name,
'short_name' => e.user.short_name,
'id' => e.user.id,
'created_at' => e.user.created_at.iso8601
}
}
h['grades'] = {
'html_url' => course_student_grades_url(@course, e.user),
'final_score' => nil,
'current_score' => nil,
'final_grade' => nil,
'current_grade' => nil,
} if e.student?
h.merge!(
'last_activity_at' => nil,
'last_attended_at' => nil,
'total_activity_time' => 0
) if e.user == @user
h
})
end
it "returns an empty array when no user enrollments match a filter" do
site_admin_user(:active_all => true)
json = api_call(:get, "#{@user_path}?type[]=TeacherEnrollment",
@user_params.merge(:type => %w{TeacherEnrollment}))
expect(json).to be_empty
end
end
end
describe "enrollment invitations" do
it "accepts invitation" do
course_with_student_logged_in(active_course: true, active_user: true)
json = api_call_as_user(@student, :post,
"/api/v1/courses/#{@course.id}/enrollments/#{@enrollment.id}/accept",
{ controller: 'enrollments_api', action: 'accept',
course_id: @course.to_param, id: @enrollment.to_param, format: :json })
expect(json['success']).to eq true
expect(@enrollment.reload).to be_active
end
it "accepts one invitation when there are multiple sections" do
course = course_factory({ active_course: true })
s1 = course.course_sections.create
s2 = course.course_sections.create
en1 = course_with_student(active_user: true, course: course, section: s1, enrollment_state: 'invited')
en2 = course_with_student(course: course, section: s2, enrollment_state: 'invited', allow_multiple_enrollments: true, user: @student)
json = api_call_as_user(@student, :post,
"/api/v1/courses/#{@course.id}/enrollments/#{en1.id}/accept",
{ controller: 'enrollments_api', action: 'accept',
course_id: @course.to_param, id: en1.to_param, format: :json })
expect(json['success']).to eq true
expect(en1.reload.workflow_state).to eq 'active'
expect(en2.reload.workflow_state).to eq 'invited'
json = api_call_as_user(@student, :post,
"/api/v1/courses/#{@course.id}/enrollments/#{en2.id}/accept",
{ controller: 'enrollments_api', action: 'accept',
course_id: @course.to_param, id: en2.to_param, format: :json })
expect(json['success']).to eq true
expect(en2.reload.workflow_state).to eq 'active'
end
it 'rejects invitation' do
course_with_student_logged_in(active_course: true, active_user: true)
json = api_call_as_user(@student, :post,
"/api/v1/courses/#{@course.id}/enrollments/#{@enrollment.id}/reject",
{ controller: 'enrollments_api', action: 'reject',
course_id: @course.to_param, id: @enrollment.to_param, format: :json })
expect(json['success']).to eq true
expect(@enrollment.reload.workflow_state).to eq 'rejected'
end
it 'rejects and then accept' do
course_with_student_logged_in(active_course: true, active_user: true)
json = api_call_as_user(@student, :post,
"/api/v1/courses/#{@course.id}/enrollments/#{@enrollment.id}/reject",
{ controller: 'enrollments_api', action: 'reject',
course_id: @course.to_param, id: @enrollment.to_param, format: :json })
expect(json['success']).to eq true
expect(@enrollment.reload.workflow_state).to eq 'rejected'
json = api_call_as_user(@student, :post,
"/api/v1/courses/#{@course.id}/enrollments/#{@enrollment.id}/accept",
{ controller: 'enrollments_api', action: 'accept',
course_id: @course.to_param, id: @enrollment.to_param, format: :json })
expect(json['success']).to eq true
expect(@enrollment.reload.workflow_state).to eq 'active'
end
it 'does not accept after course has ended' do
course_with_student_logged_in(active_course: true, active_user: true)
@course.soft_conclude!
@course.save
json = api_call_as_user(@student, :post,
"/api/v1/courses/#{@course.id}/enrollments/#{@enrollment.id}/accept",
{ controller: 'enrollments_api', action: 'accept',
course_id: @course.to_param, id: @enrollment.to_param, format: :json })
expect(response.code).to eq '400'
expect(json['error']).to eq 'no current invitation'
end
it 'does not reject after course has ended' do
course_with_student_logged_in(active_course: true, active_user: true)
@course.soft_conclude!
@course.save
json = api_call_as_user(@student, :post,
"/api/v1/courses/#{@course.id}/enrollments/#{@enrollment.id}/reject",
{ controller: 'enrollments_api', action: 'reject',
course_id: @course.to_param, id: @enrollment.to_param, format: :json })
expect(response.code).to eq '400'
expect(json['error']).to eq 'no current invitation'
end
it 'does not accept if self_enrolled' do
course_with_student_logged_in(active_course: true, active_user: true)
@enrollment.self_enrolled = true
@enrollment.save
json = api_call_as_user(@student, :post,
"/api/v1/courses/#{@course.id}/enrollments/#{@enrollment.id}/accept",
{ controller: 'enrollments_api', action: 'accept',
course_id: @course.to_param, id: @enrollment.to_param, format: :json })
expect(response.code).to eq '400'
expect(json['error']).to eq 'self enroll'
end
it 'does not reject if self_enrolled' do
course_with_student_logged_in(active_course: true, active_user: true)
@enrollment.self_enrolled = true
@enrollment.save
json = api_call_as_user(@student, :post,
"/api/v1/courses/#{@course.id}/enrollments/#{@enrollment.id}/reject",
{ controller: 'enrollments_api', action: 'reject',
course_id: @course.to_param, id: @enrollment.to_param, format: :json })
expect(response.code).to eq '400'
expect(json['error']).to eq 'self enroll'
end
it 'does not accept if inactive' do
course_with_student_logged_in(active_course: true, active_user: true)
@enrollment.workflow_state = 'inactive'
@enrollment.save
json = api_call_as_user(@student, :post,
"/api/v1/courses/#{@course.id}/enrollments/#{@enrollment.id}/accept",
{ controller: 'enrollments_api', action: 'accept',
course_id: @course.to_param, id: @enrollment.to_param, format: :json })
expect(response.code).to eq '400'
expect(json['error']).to eq 'membership not activated'
end
it 'does not reject if inactive' do
course_with_student_logged_in(active_course: true, active_user: true)
@enrollment.workflow_state = 'inactive'
@enrollment.save
json = api_call_as_user(@student, :post,
"/api/v1/courses/#{@course.id}/enrollments/#{@enrollment.id}/reject",
{ controller: 'enrollments_api', action: 'reject',
course_id: @course.to_param, id: @enrollment.to_param, format: :json })
expect(response.code).to eq '400'
expect(json['error']).to eq 'membership not activated'
end
end
end