canvas-lms/lib/api/v1/user.rb

334 lines
13 KiB
Ruby

#
# Copyright (C) 2011 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
# includes Enrollment json helpers
module Api::V1::User
include Api::V1::Json
include AvatarHelper
API_USER_JSON_OPTS = {
:only => %w(id name).freeze,
:methods => %w(sortable_name short_name).freeze
}.freeze
def user_json_preloads(users, preload_email=false, opts={})
# for User#account
ActiveRecord::Associations::Preloader.new.preload(users, :pseudonym => :account)
# pseudonyms for SisPseudonym
# pseudonyms account for Pseudonym#works_for_account?
ActiveRecord::Associations::Preloader.new.preload(users, pseudonyms: :account) if user_json_is_admin?
if preload_email && (no_email_users = users.reject(&:email_cached?)).present?
# communication_channels for User#email if it is not cached
ActiveRecord::Associations::Preloader.new.preload(no_email_users, :communication_channels)
end
if opts[:group_memberships]
ActiveRecord::Associations::Preloader.new.preload(users, :group_memberships)
end
end
def user_json(user, current_user, session, includes = [], context = @context, enrollments = nil, excludes = [])
includes ||= []
excludes ||= []
api_json(user, current_user, session, API_USER_JSON_OPTS).tap do |json|
enrollment_json_opts = { current_grading_period_scores: includes.include?('current_grading_period_scores') }
if !excludes.include?('pseudonym') && user_json_is_admin?(context, current_user)
include_root_account = @domain_root_account.trust_exists?
pseudonym = SisPseudonym.for(user, @domain_root_account, type: :implicit, require_sis: false)
enrollment_json_opts[:sis_pseudonym] = pseudonym if pseudonym&.sis_user_id
# the sis fields on pseudonym are poorly named -- sis_user_id is
# the id in the SIS import data, where on every other table
# that's called sis_source_id.
if user_can_read_sis_data?(current_user, context)
json.merge! :sis_user_id => pseudonym&.sis_user_id,
:integration_id => pseudonym&.integration_id
# TODO: don't send sis_login_id; it's garbage data
if @domain_root_account.settings['return_sis_login_id'] == 'true'
json.merge! :sis_login_id => pseudonym&.unique_id
end
end
json[:sis_import_id] = pseudonym&.sis_batch_id if @domain_root_account.grants_right?(current_user, session, :manage_sis)
json[:root_account] = HostUrl.context_host(pseudonym&.account) if include_root_account
if pseudonym
json[:login_id] = pseudonym.unique_id
end
end
if includes.include?('avatar_url') && user.account.service_enabled?(:avatars)
json[:avatar_url] = avatar_url_for_user(user, blank_fallback)
end
if enrollments
json[:enrollments] = enrollments.map do |enrollment|
enrollment_json(enrollment, current_user, session, includes, enrollment_json_opts)
end
end
# include a permissions check here to only allow teachers and admins
# to see user email addresses.
if includes.include?('email') && !excludes.include?('personal_info') && context.grants_right?(current_user, session, :read_roster)
json[:email] = user.email
end
if includes.include?('bio') && !excludes.include?('personal_info') && @domain_root_account.enable_profiles? && user.profile
json[:bio] = user.profile.bio
end
if includes.include?('sections')
json[:sections] = user.enrollments.
map(&:course_section).compact.uniq.
map(&:name).join(", ")
end
# make sure this only runs if user_json_preloads has
# been called with {group_memberships: true} in opts
if includes.include?('group_ids')
context_group_ids = get_context_groups(context)
json[:group_ids] = context_group_ids & group_ids(user)
end
json[:locale] = user.locale if includes.include?('locale')
json[:confirmation_url] = user.communication_channels.email.first.try(:confirmation_url) if includes.include?('confirmation_url')
if includes.include?('last_login')
last_login = user.read_attribute(:last_login)
if last_login.is_a?(String)
Time.use_zone('UTC') { last_login = Time.zone.parse(last_login) }
end
json[:last_login] = last_login.try(:iso8601)
end
if includes.include?('permissions')
json[:permissions] = {
:can_update_name => user.user_can_edit_name?,
:can_update_avatar => service_enabled?(:avatars)
}
end
if includes.include?('terms_of_use')
json[:terms_of_use] = !!user.preferences[:accepted_terms]
end
if includes.include?('custom_links')
json[:custom_links] = roster_user_custom_links(user)
end
if includes.include?('time_zone')
zone = user.time_zone || @domain_root_account.try(:default_time_zone) || Time.zone
json[:time_zone] = zone.name
end
if includes.include?('lti_id')
json[:lti_id] = user.lti_context_id
end
end
end
def users_json(users, current_user, session, includes = [], context = @context, enrollments = nil, excludes = [])
if includes.include?('sections')
ActiveRecord::Associations::Preloader.new.preload(users, enrollments: :course_section)
end
if includes.include?('group_ids') && !context.is_a?(Groups)
ActiveRecord::Associations::Preloader.new.preload(context, :groups)
end
users.map{ |user| user_json(user, current_user, session, includes, context, enrollments, excludes) }
end
# this mini-object is used for secondary user responses, when we just want to
# provide enough information to display a user.
# for instance, discussion entries return this json as a sub-object.
#
# if parent_context is given, the html_url will be scoped to that context, so:
# /courses/X/users/Y
# otherwise it'll just be:
# /users/Y
# keep in mind the latter form is only accessible if the user has a public profile
# (or if the api caller is an admin)
#
# if parent_context is :profile, the html_url will always be the user's
# public profile url, regardless of @current_user permissions
def user_display_json(user, parent_context = nil)
return {} unless user
participant_url = case parent_context
when :profile
user_profile_url(user)
when nil, false
user_url(user)
else
polymorphic_url([parent_context, user])
end
hash = {
id: user.id,
display_name: user.short_name,
avatar_image_url: avatar_url_for_user(user, blank_fallback),
html_url: participant_url
}
hash[:fake_student] = true if user.fake_student?
hash
end
# optimization hint, currently user only needs to pull pseudonyms from the db
# if a site admin is making the request or they can manage_students
def user_json_is_admin?(context = @context, current_user = @current_user)
return false if context.nil? || current_user.nil?
@user_json_is_admin ||= {}
@user_json_is_admin[[context.class.name, context.global_id, current_user.global_id]] ||= (
if context.is_a?(::UserProfile)
permissions_context = permissions_account = @domain_root_account
else
permissions_context = context
permissions_account = context.is_a?(Account) ? context : context.account
end
!!(
permissions_context.grants_any_right?(current_user, :manage_students, :read_sis) ||
permissions_account.membership_for_user(current_user) ||
permissions_account.root_account.grants_right?(current_user, :manage_sis)
)
)
end
API_ENROLLMENT_JSON_OPTS = [:id,
:root_account_id,
:user_id,
:course_id,
:course_section_id,
:associated_user_id,
:limit_privileges_to_course_section,
:workflow_state,
:updated_at,
:created_at,
:start_at,
:end_at,
:type]
def enrollment_json(enrollment, user, session, includes = [], opts = {})
api_json(enrollment, user, session, :only => API_ENROLLMENT_JSON_OPTS).tap do |json|
json[:enrollment_state] = json.delete('workflow_state')
if enrollment.course.workflow_state == 'deleted' || enrollment.course_section.workflow_state == 'deleted'
json[:enrollment_state] = 'deleted'
end
json[:role] = enrollment.role.name
json[:role_id] = enrollment.role_id
json[:last_activity_at] = enrollment.last_activity_at
json[:total_activity_time] = enrollment.total_activity_time
if enrollment.root_account.grants_right?(user, session, :manage_sis)
json[:sis_import_id] = enrollment.sis_batch_id
end
if enrollment.student?
json[:grades] = grades_hash(enrollment, user, opts)
end
if user_can_read_sis_data?(@current_user, enrollment.course)
json[:sis_account_id] = enrollment.course.account.sis_source_id
json[:sis_course_id] = enrollment.course.sis_source_id
json[:course_integration_id] = enrollment.course.integration_id
json[:sis_section_id] = enrollment.course_section.sis_source_id
json[:section_integration_id] = enrollment.course_section.integration_id
pseudonym = opts.key?(:sis_pseudonym) ? opts[:sis_pseudonym] : sis_pseudonym_for(enrollment.user)
json[:sis_user_id] = pseudonym.try(:sis_user_id)
end
json[:html_url] = course_user_url(enrollment.course_id, enrollment.user_id)
user_includes = includes.include?('avatar_url') ? ['avatar_url'] : []
user_includes << 'group_ids' if includes.include?('group_ids')
json[:user] = user_json(enrollment.user, user, session, user_includes, @context, nil, []) if includes.include?(:user)
if includes.include?('locked')
lockedbysis = enrollment.defined_by_sis?
lockedbysis &&= !enrollment.course.account.grants_right?(@current_user, session, :manage_account_settings)
json[:locked] = lockedbysis
end
if includes.include?('observed_users') && enrollment.observer? && enrollment.associated_user && !enrollment.associated_user.deleted?
json[:observed_user] = user_json(enrollment.associated_user, user, session, user_includes, @context, enrollment.associated_user.not_ended_enrollments.all_student.shard(enrollment).where(:course_id => enrollment.course_id))
end
if includes.include?('can_be_removed')
json[:can_be_removed] = (!enrollment.defined_by_sis? || context.grants_right?(@current_user, session, :manage_account_settings)) &&
enrollment.can_be_deleted_by(@current_user, @context, session)
end
end
end
private
def grades_hash(enrollment, user, opts = {})
grades = {
html_url: course_student_grades_url(enrollment.course_id, enrollment.user_id)
}
if grade_permissions?(user, enrollment)
gpid = grading_period(enrollment.course, opts)&.id
grades[:current_score] = enrollment.computed_current_score(grading_period_id: gpid)
grades[:current_grade] = enrollment.computed_current_grade(grading_period_id: gpid)
grades[:final_score] = enrollment.computed_final_score(grading_period_id: gpid)
grades[:final_grade] = enrollment.computed_final_grade(grading_period_id: gpid)
grades[:grading_period_id] = gpid if opts[:current_grading_period_scores]
end
grades
end
def grading_period(course, opts)
return opts[:grading_period] if opts[:grading_period]
return nil unless opts[:current_grading_period_scores]
GradingPeriod.current_period_for(course)
end
def grade_permissions?(user, enrollment)
course = enrollment.course
(user.id == enrollment.user_id && !course.hide_final_grades?) ||
course.grants_any_right?(user, :manage_grades, :view_all_grades) ||
enrollment.user.grants_right?(user, :read_as_parent)
end
def get_context_groups(context)
# make sure to preload groups if using this
context.is_a?(Group) ?
[context.id] :
context.groups.map(&:id)
end
def sis_id_context(context)
case context
when Account, Course
context
when Group
context.context
else
@domain_root_account
end
end
def user_can_read_sis_data?(user, context)
sis_id_context(context).grants_right?(user, :read_sis) || @domain_root_account.grants_right?(user, :manage_sis)
end
def sis_pseudonym_for(user)
SisPseudonym.for(user, @domain_root_account, type: :trusted)
end
def group_ids(user)
if user.group_memberships.loaded?
user.group_memberships.map(&:group_id)
else
user.group_memberships.pluck(:group_id)
end
end
end