2441 lines
92 KiB
Ruby
2441 lines
92 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/>.
|
|
#
|
|
|
|
require 'atom'
|
|
|
|
# @API Users
|
|
# API for accessing information on the current and other users.
|
|
#
|
|
# Throughout this API, the `:user_id` parameter can be replaced with `self` as
|
|
# a shortcut for the id of the user accessing the API. For instance,
|
|
# `users/:user_id/page_views` can be accessed as `users/self/page_views` to
|
|
# access the current user's page views.
|
|
#
|
|
# @model UserDisplay
|
|
# {
|
|
# "id": "UserDisplay",
|
|
# "description": "This mini-object is used for secondary user responses, when we just want to provide enough information to display a user.",
|
|
# "properties": {
|
|
# "id": {
|
|
# "description": "The ID of the user.",
|
|
# "example": 2,
|
|
# "type": "integer",
|
|
# "format": "int64"
|
|
# },
|
|
# "short_name": {
|
|
# "description": "A short name the user has selected, for use in conversations or other less formal places through the site.",
|
|
# "example": "Shelly",
|
|
# "type": "string"
|
|
# },
|
|
# "avatar_image_url": {
|
|
# "description": "If avatars are enabled, this field will be included and contain a url to retrieve the user's avatar.",
|
|
# "example": "https://en.gravatar.com/avatar/d8cb8c8cd40ddf0cd05241443a591868?s=80&r=g",
|
|
# "type": "string"
|
|
# },
|
|
# "html_url": {
|
|
# "description": "URL to access user, either nested to a context or directly.",
|
|
# "example": "https://school.instructure.com/courses/:course_id/users/:user_id",
|
|
# "type": "string"
|
|
# }
|
|
# }
|
|
# }
|
|
#
|
|
#
|
|
# @model User
|
|
# {
|
|
# "id": "User",
|
|
# "description": "A Canvas user, e.g. a student, teacher, administrator, observer, etc.",
|
|
# "required": ["id"],
|
|
# "properties": {
|
|
# "id": {
|
|
# "description": "The ID of the user.",
|
|
# "example": 2,
|
|
# "type": "integer",
|
|
# "format": "int64"
|
|
# },
|
|
# "name": {
|
|
# "description": "The name of the user.",
|
|
# "example": "Sheldon Cooper",
|
|
# "type": "string"
|
|
# },
|
|
# "sortable_name": {
|
|
# "description": "The name of the user that is should be used for sorting groups of users, such as in the gradebook.",
|
|
# "example": "Cooper, Sheldon",
|
|
# "type": "string"
|
|
# },
|
|
# "short_name": {
|
|
# "description": "A short name the user has selected, for use in conversations or other less formal places through the site.",
|
|
# "example": "Shelly",
|
|
# "type": "string"
|
|
# },
|
|
# "sis_user_id": {
|
|
# "description": "The SIS ID associated with the user. This field is only included if the user came from a SIS import and has permissions to view SIS information.",
|
|
# "example": "SHEL93921",
|
|
# "type": "string"
|
|
# },
|
|
# "sis_import_id": {
|
|
# "description": "The id of the SIS import. This field is only included if the user came from a SIS import and has permissions to manage SIS information.",
|
|
# "example": "18",
|
|
# "type": "integer",
|
|
# "format": "int64"
|
|
# },
|
|
# "sis_login_id": {
|
|
# "description": "DEPRECATED: The SIS login ID associated with the user. Please use the sis_user_id or login_id. This field will be removed in a future version of the API.",
|
|
# "type": "string"
|
|
# },
|
|
# "integration_id": {
|
|
# "description": "The integration_id associated with the user. This field is only included if the user came from a SIS import and has permissions to view SIS information.",
|
|
# "example": "ABC59802",
|
|
# "type": "string"
|
|
# },
|
|
# "login_id": {
|
|
# "description": "The unique login id for the user. This is what the user uses to log in to Canvas.",
|
|
# "example": "sheldon@caltech.example.com",
|
|
# "type": "string"
|
|
# },
|
|
# "avatar_url": {
|
|
# "description": "If avatars are enabled, this field will be included and contain a url to retrieve the user's avatar.",
|
|
# "example": "https://en.gravatar.com/avatar/d8cb8c8cd40ddf0cd05241443a591868?s=80&r=g",
|
|
# "type": "string"
|
|
# },
|
|
# "enrollments": {
|
|
# "description": "Optional: This field can be requested with certain API calls, and will return a list of the users active enrollments. See the List enrollments API for more details about the format of these records.",
|
|
# "type": "array",
|
|
# "items": { "$ref": "Enrollment" }
|
|
# },
|
|
# "email": {
|
|
# "description": "Optional: This field can be requested with certain API calls, and will return the users primary email address.",
|
|
# "example": "sheldon@caltech.example.com",
|
|
# "type": "string"
|
|
# },
|
|
# "locale": {
|
|
# "description": "Optional: This field can be requested with certain API calls, and will return the users locale in RFC 5646 format.",
|
|
# "example": "tlh",
|
|
# "type": "string"
|
|
# },
|
|
# "last_login": {
|
|
# "description": "Optional: This field is only returned in certain API calls, and will return a timestamp representing the last time the user logged in to canvas.",
|
|
# "example": "2012-05-30T17:45:25Z",
|
|
# "type": "string",
|
|
# "format": "date-time"
|
|
# },
|
|
# "time_zone": {
|
|
# "description": "Optional: This field is only returned in certain API calls, and will return the IANA time zone name of the user's preferred timezone.",
|
|
# "example": "America/Denver",
|
|
# "type": "string"
|
|
# },
|
|
# "bio": {
|
|
# "description": "Optional: The user's bio.",
|
|
# "example": "I like the Muppets.",
|
|
# "type": "string"
|
|
# }
|
|
# }
|
|
# }
|
|
#
|
|
#
|
|
#
|
|
class UsersController < ApplicationController
|
|
include Delicious
|
|
include SearchHelper
|
|
include SectionTabHelper
|
|
include I18nUtilities
|
|
include CustomColorHelper
|
|
|
|
before_action :require_user, :only => [:grades, :merge, :kaltura_session,
|
|
:ignore_item, :ignore_stream_item, :close_notification, :mark_avatar_image,
|
|
:user_dashboard, :toggle_recent_activity_dashboard, :toggle_hide_dashcard_color_overlays,
|
|
:masquerade, :external_tool, :dashboard_sidebar, :settings, :activity_stream,
|
|
:activity_stream_summary]
|
|
before_action :require_registered_user, :only => [:delete_user_service,
|
|
:create_user_service]
|
|
before_action :reject_student_view_student, :only => [:delete_user_service,
|
|
:create_user_service, :merge, :user_dashboard, :masquerade]
|
|
skip_before_action :load_user, :only => [:create_self_registered_user]
|
|
before_action :require_self_registration, :only => [:new, :create, :create_self_registered_user]
|
|
|
|
def grades
|
|
@user = User.where(id: params[:user_id]).first if params[:user_id].present?
|
|
@user ||= @current_user
|
|
if authorized_action(@user, @current_user, :read_grades)
|
|
crumb_url = polymorphic_url([@current_user]) if @user.grants_right?(@current_user, session, :view_statistics)
|
|
add_crumb(@current_user.short_name, crumb_url)
|
|
add_crumb(t('crumbs.grades', 'Grades'), grades_path)
|
|
|
|
current_active_enrollments = @user.
|
|
enrollments.
|
|
current.
|
|
preload(:course, :enrollment_state, :scores).
|
|
shard(@user).
|
|
to_a
|
|
|
|
@presenter = GradesPresenter.new(current_active_enrollments)
|
|
|
|
if @presenter.has_single_enrollment?
|
|
redirect_to course_grades_url(@presenter.single_enrollment.course_id)
|
|
return
|
|
end
|
|
|
|
@grading_periods = collected_grading_periods_for_presenter(
|
|
@presenter, params[:course_id], params[:grading_period_id])
|
|
@grades = grades_for_presenter(@presenter, @grading_periods)
|
|
js_env :grades_for_student_url => grades_for_student_url
|
|
|
|
ActiveRecord::Associations::Preloader.new.preload(@observed_enrollments, :course)
|
|
end
|
|
end
|
|
|
|
def grades_for_student
|
|
enrollment = Enrollment.active.find(params[:enrollment_id])
|
|
return render_unauthorized_action unless enrollment.grants_right?(@current_user, session, :read_grades)
|
|
|
|
grading_period_id = generate_grading_period_id(params[:grading_period_id])
|
|
render json: {
|
|
grade: enrollment.computed_current_score(grading_period_id: grading_period_id),
|
|
hide_final_grades: enrollment.course.hide_final_grades?
|
|
}
|
|
end
|
|
|
|
def oauth
|
|
if !feature_and_service_enabled?(params[:service])
|
|
flash[:error] = t('service_not_enabled', "That service has not been enabled")
|
|
return redirect_to(user_profile_url(@current_user))
|
|
end
|
|
return_to_url = params[:return_to] || user_profile_url(@current_user)
|
|
if params[:service] == "google_drive"
|
|
redirect_uri = oauth_success_url(:service => 'google_drive')
|
|
session[:oauth_gdrive_nonce] = SecureRandom.hex
|
|
state = Canvas::Security.create_jwt(redirect_uri: redirect_uri, return_to_url: return_to_url, nonce: session[:oauth_gdrive_nonce])
|
|
redirect_to GoogleDrive::Client.auth_uri(google_drive_client, state)
|
|
elsif params[:service] == "twitter"
|
|
success_url = oauth_success_url(:service => 'twitter')
|
|
request_token = Twitter::Connection.request_token(success_url)
|
|
OauthRequest.create(
|
|
:service => 'twitter',
|
|
:token => request_token.token,
|
|
:secret => request_token.secret,
|
|
:return_url => return_to_url,
|
|
:user => @current_user,
|
|
:original_host_with_port => request.host_with_port
|
|
)
|
|
redirect_to request_token.authorize_url
|
|
elsif params[:service] == "linked_in"
|
|
success_url = oauth_success_url(:service => 'linked_in')
|
|
request_token = LinkedIn::Connection.request_token(success_url)
|
|
|
|
OauthRequest.create(
|
|
:service => 'linked_in',
|
|
:token => request_token.token,
|
|
:secret => request_token.secret,
|
|
:return_url => return_to_url,
|
|
:user => @current_user,
|
|
:original_host_with_port => request.host_with_port
|
|
)
|
|
|
|
redirect_to request_token.authorize_url
|
|
end
|
|
end
|
|
|
|
def oauth_success
|
|
oauth_request = nil
|
|
if params[:oauth_token]
|
|
oauth_request = OauthRequest.where(token: params[:oauth_token], service: params[:service]).first
|
|
elsif params[:code] && params[:state] && params[:service] == 'google_drive'
|
|
|
|
begin
|
|
|
|
client = google_drive_client
|
|
client.authorization.code = params[:code]
|
|
client.authorization.fetch_access_token!
|
|
|
|
# we should look into consolidating this and connection.rb
|
|
drive = Rails.cache.fetch(['google_drive_v2'].cache_key) do
|
|
client.discovered_api('drive', 'v2')
|
|
end
|
|
|
|
result = client.execute!(:api_method => drive.about.get)
|
|
|
|
if result.status == 200
|
|
user_info = result.data
|
|
else
|
|
raise "Error getting user info from Google"
|
|
end
|
|
|
|
json = Canvas::Security.decode_jwt(params[:state])
|
|
render_unauthorized_action and return unless json['nonce'] && json['nonce'] == session[:oauth_gdrive_nonce]
|
|
session.delete(:oauth_gdrive_nonce)
|
|
|
|
if logged_in_user
|
|
UserService.register(
|
|
:service => "google_drive",
|
|
:service_domain => "drive.google.com",
|
|
:token => client.authorization.refresh_token,
|
|
:secret => client.authorization.access_token,
|
|
:user => logged_in_user,
|
|
:service_user_id => user_info['permissionId'],
|
|
:service_user_name => user_info['user']['emailAddress']
|
|
)
|
|
else
|
|
session[:oauth_gdrive_access_token] = client.authorization.access_token
|
|
session[:oauth_gdrive_refresh_token] = client.authorization.refresh_token
|
|
end
|
|
|
|
flash[:notice] = t('google_drive_added', "Google Drive account successfully added!")
|
|
return redirect_to(json['return_to_url'])
|
|
rescue Google::APIClient::ClientError => e
|
|
Canvas::Errors.capture_exception(:oauth, e)
|
|
|
|
flash[:error] = e.to_s
|
|
end
|
|
return redirect_to(user_profile_url(@current_user))
|
|
end
|
|
|
|
if !oauth_request || (request.host_with_port == oauth_request.original_host_with_port && oauth_request.user != @current_user)
|
|
flash[:error] = t('oauth_fail', "OAuth Request failed. Couldn't find valid request")
|
|
redirect_to (@current_user ? user_profile_url(@current_user) : root_url)
|
|
elsif request.host_with_port != oauth_request.original_host_with_port
|
|
url = url_for request.parameters.merge(:host => oauth_request.original_host_with_port, :only_path => false)
|
|
redirect_to url
|
|
else
|
|
if params[:service] == "linked_in"
|
|
begin
|
|
raise "No OAuth LinkedIn User" unless oauth_request.user
|
|
|
|
linkedin = LinkedIn::Connection.from_request_token(
|
|
oauth_request.token,
|
|
oauth_request.secret,
|
|
params[:oauth_verifier]
|
|
)
|
|
|
|
UserService.register(
|
|
:service => "linked_in",
|
|
:access_token => linkedin.access_token,
|
|
:user => oauth_request.user,
|
|
:service_domain => "linked_in.com",
|
|
:service_user_id => linkedin.service_user_id,
|
|
:service_user_name => linkedin.service_user_name,
|
|
:service_user_url => linkedin.service_user_url
|
|
)
|
|
oauth_request.destroy
|
|
|
|
flash[:notice] = t('linkedin_added', "LinkedIn account successfully added!")
|
|
rescue => e
|
|
Canvas::Errors.capture_exception(:oauth, e)
|
|
flash[:error] = t('linkedin_fail', "LinkedIn authorization failed. Please try again")
|
|
end
|
|
else
|
|
begin
|
|
raise "No OAuth Twitter User" unless oauth_request.user
|
|
|
|
twitter = Twitter::Connection.from_request_token(
|
|
oauth_request.token,
|
|
oauth_request.secret,
|
|
params[:oauth_verifier]
|
|
)
|
|
UserService.register(
|
|
:service => "twitter",
|
|
:access_token => twitter.access_token,
|
|
:user => oauth_request.user,
|
|
:service_domain => "twitter.com",
|
|
:service_user_id => twitter.service_user_id,
|
|
:service_user_name => twitter.service_user_name
|
|
)
|
|
oauth_request.destroy
|
|
|
|
flash[:notice] = t('twitter_added', "Twitter access authorized!")
|
|
rescue => e
|
|
Canvas::Errors.capture_exception(:oauth, e)
|
|
flash[:error] = t('twitter_fail_whale', "Twitter authorization failed. Please try again")
|
|
end
|
|
end
|
|
return_to(oauth_request.return_url, user_profile_url(@current_user))
|
|
end
|
|
end
|
|
|
|
# @API List users in account
|
|
# Retrieve the list of users associated with this account.
|
|
#
|
|
# @argument search_term [String]
|
|
# The partial name or full ID of the users to match and return in the
|
|
# results list. Must be at least 3 characters.
|
|
#
|
|
# Note that the API will prefer matching on canonical user ID if the ID has
|
|
# a numeric form. It will only search against other fields if non-numeric
|
|
# in form, or if the numeric value doesn't yield any matches. Queries by
|
|
# administrative users will search on SIS ID, login ID, name, or email
|
|
# address; non-administrative queries will only be compared against name.
|
|
#
|
|
# @example_request
|
|
# curl https://<canvas>/api/v1/accounts/self/users?search_term=<search value> \
|
|
# -X GET \
|
|
# -H 'Authorization: Bearer <token>'
|
|
#
|
|
# @returns [User]
|
|
def index
|
|
get_context
|
|
if authorized_action(@context, @current_user, :read_roster)
|
|
@root_account = @context.root_account
|
|
@query = (params[:user] && params[:user][:name]) || params[:term]
|
|
js_env :ACCOUNT => account_json(@domain_root_account, nil, session, ['registration_settings'])
|
|
Shackles.activate(:slave) do
|
|
if @context && @context.is_a?(Account) && @query
|
|
@users = @context.users_name_like(@query)
|
|
elsif params[:enrollment_term_id].present? && @root_account == @context
|
|
@users = @context.fast_all_users.
|
|
where("EXISTS (?)", Enrollment.where("enrollments.user_id=users.id").
|
|
joins(:course).
|
|
where(Enrollment::QueryBuilder.new(:active).conditions).
|
|
where(courses: { enrollment_term_id: params[:enrollment_term_id]}))
|
|
elsif !api_request?
|
|
@users = @context.fast_all_users
|
|
end
|
|
|
|
if api_request?
|
|
search_term = params[:search_term].presence
|
|
page_opts = {}
|
|
if search_term
|
|
users = UserSearch.for_user_in_context(search_term, @context, @current_user, session)
|
|
page_opts[:total_entries] = nil # doesn't calculate a total count
|
|
else
|
|
users = UserSearch.scope_for(@context, @current_user)
|
|
end
|
|
|
|
includes = (params[:include] || []) & %w{avatar_url email last_login time_zone}
|
|
users = users.with_last_login if includes.include?('last_login')
|
|
users = Api.paginate(users, self, api_v1_account_users_url, page_opts)
|
|
user_json_preloads(users, includes.include?('email'))
|
|
return render :json => users.map { |u| user_json(u, @current_user, session, includes)}
|
|
else
|
|
@users ||= []
|
|
@users = @users.paginate(:page => params[:page])
|
|
end
|
|
|
|
respond_to do |format|
|
|
if @users.length == 1 && params[:term]
|
|
format.html {
|
|
redirect_to(named_context_url(@context, :context_user_url, @users.first))
|
|
}
|
|
else
|
|
@enrollment_terms = []
|
|
if @root_account == @context
|
|
@enrollment_terms = @context.enrollment_terms.active
|
|
end
|
|
format.html
|
|
end
|
|
format.json {
|
|
cancel_cache_buster
|
|
expires_in 30.minutes
|
|
render(:json => @users.map { |u| { :label => u.name, :id => u.id } })
|
|
}
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
before_action :require_password_session, :only => [:masquerade]
|
|
def masquerade
|
|
@user = api_find(User, params[:user_id])
|
|
return render_unauthorized_action unless @user.can_masquerade?(@real_current_user || @current_user, @domain_root_account)
|
|
if request.post?
|
|
if @user == @real_current_user
|
|
session.delete(:become_user_id)
|
|
session.delete(:enrollment_uuid)
|
|
else
|
|
session[:become_user_id] = params[:user_id]
|
|
end
|
|
return_url = session[:masquerade_return_to]
|
|
session.delete(:masquerade_return_to)
|
|
@current_user.associate_with_shard(@user.shard, :shadow) if PageView.db?
|
|
return return_to(return_url, request.referer || dashboard_url)
|
|
end
|
|
end
|
|
|
|
helper_method :show_planner?
|
|
def show_planner?
|
|
return false unless @current_user && @current_user.preferences
|
|
if @current_user.preferences[:dashboard_view]
|
|
@current_user.preferences[:dashboard_view] == 'planner'
|
|
else
|
|
false
|
|
end
|
|
end
|
|
|
|
def user_dashboard
|
|
session.delete(:parent_registration) if session[:parent_registration]
|
|
check_incomplete_registration
|
|
get_context
|
|
|
|
# dont show crumbs on dashboard because it does not make sense to have a breadcrumb
|
|
# trail back to home if you are already home
|
|
clear_crumbs
|
|
|
|
@show_footer = true
|
|
|
|
if request.path =~ %r{\A/dashboard\z}
|
|
return redirect_to(dashboard_url, :status => :moved_permanently)
|
|
end
|
|
disable_page_views if @current_pseudonym && @current_pseudonym.unique_id == "pingdom@instructure.com"
|
|
|
|
js_env({
|
|
:DASHBOARD_SIDEBAR_URL => dashboard_sidebar_url,
|
|
:PREFERENCES => {
|
|
:recent_activity_dashboard => @current_user.preferences[:dashboard_view] == 'activity' || @current_user.preferences[:recent_activity_dashboard],
|
|
:hide_dashcard_color_overlays => @current_user.preferences[:hide_dashcard_color_overlays],
|
|
:custom_colors => @current_user.custom_colors,
|
|
:show_planner => show_planner?
|
|
},
|
|
:STUDENT_PLANNER_ENABLED => planner_enabled?
|
|
})
|
|
|
|
@announcements = AccountNotification.for_user_and_account(@current_user, @domain_root_account)
|
|
@pending_invitations = @current_user.cached_invitations(:include_enrollment_uuid => session[:enrollment_uuid], :preload_course => true)
|
|
@stream_items = @current_user.try(:cached_recent_stream_items) || []
|
|
end
|
|
|
|
def cached_upcoming_events(user)
|
|
Rails.cache.fetch(['cached_user_upcoming_events', user].cache_key,
|
|
:expires_in => 3.minutes) do
|
|
user.upcoming_events :context_codes => ([user.asset_string] + user.cached_context_codes)
|
|
end
|
|
end
|
|
|
|
def cached_submissions(user, upcoming_events)
|
|
Rails.cache.fetch(['cached_user_submissions2', user].cache_key,
|
|
:expires_in => 3.minutes) do
|
|
assignments = upcoming_events.select{ |e| e.is_a?(Assignment) }
|
|
Shard.partition_by_shard(assignments) do |shard_assignments|
|
|
Submission.
|
|
select([:id, :assignment_id, :score, :grade, :workflow_state, :updated_at]).
|
|
where(:assignment_id => shard_assignments, :user_id => user)
|
|
end
|
|
end
|
|
end
|
|
|
|
def prepare_current_user_dashboard_items
|
|
if @current_user
|
|
@upcoming_events =
|
|
cached_upcoming_events(@current_user)
|
|
@current_user_submissions =
|
|
cached_submissions(@current_user, @upcoming_events)
|
|
else
|
|
@upcoming_events = []
|
|
end
|
|
end
|
|
|
|
def dashboard_sidebar
|
|
Shackles.activate(:slave) do
|
|
prepare_current_user_dashboard_items
|
|
|
|
if @show_recent_feedback = (@current_user.student_enrollments.active.exists?)
|
|
@recent_feedback = (@current_user && @current_user.recent_feedback) || []
|
|
end
|
|
end
|
|
|
|
render :layout => false
|
|
end
|
|
|
|
# This should be considered as deprecated in favor of the dashboard_view endpoint
|
|
# instead. DON'T USE THIS AGAIN
|
|
def toggle_recent_activity_dashboard
|
|
@current_user.preferences[:recent_activity_dashboard] =
|
|
!@current_user.preferences[:recent_activity_dashboard]
|
|
@current_user.save!
|
|
render json: {}
|
|
end
|
|
|
|
def toggle_hide_dashcard_color_overlays
|
|
@current_user.preferences[:hide_dashcard_color_overlays] =
|
|
!@current_user.preferences[:hide_dashcard_color_overlays]
|
|
@current_user.save!
|
|
render json: {}
|
|
end
|
|
|
|
def dashboard_view
|
|
if request.get?
|
|
render json: {
|
|
dashboard_view: @current_user.preferences[:dashboard_view]
|
|
}
|
|
elsif request.put?
|
|
valid_options = ['activity', 'cards', 'planner']
|
|
|
|
unless valid_options.include?(params[:dashboard_view])
|
|
return render(json: { :message => "Invalid Dashboard View Option" }, status: :bad_request)
|
|
end
|
|
|
|
@current_user.preferences[:dashboard_view] = params[:dashboard_view]
|
|
@current_user.save!
|
|
render json: {}
|
|
end
|
|
end
|
|
|
|
include Api::V1::StreamItem
|
|
|
|
# @API List the activity stream
|
|
# Returns the current user's global activity stream, paginated.
|
|
#
|
|
# There are many types of objects that can be returned in the activity
|
|
# stream. All object types have the same basic set of shared attributes:
|
|
# !!!javascript
|
|
# {
|
|
# 'created_at': '2011-07-13T09:12:00Z',
|
|
# 'updated_at': '2011-07-25T08:52:41Z',
|
|
# 'id': 1234,
|
|
# 'title': 'Stream Item Subject',
|
|
# 'message': 'This is the body text of the activity stream item. It is plain-text, and can be multiple paragraphs.',
|
|
# 'type': 'DiscussionTopic|Conversation|Message|Submission|Conference|Collaboration|AssessmentRequest...',
|
|
# 'read_state': false,
|
|
# 'context_type': 'course', // course|group
|
|
# 'course_id': 1,
|
|
# 'group_id': null,
|
|
# 'html_url': "http://..." // URL to the Canvas web UI for this stream item
|
|
# }
|
|
#
|
|
# In addition, each item type has its own set of attributes available.
|
|
#
|
|
# DiscussionTopic:
|
|
#
|
|
# !!!javascript
|
|
# {
|
|
# 'type': 'DiscussionTopic',
|
|
# 'discussion_topic_id': 1234,
|
|
# 'total_root_discussion_entries': 5,
|
|
# 'require_initial_post': true,
|
|
# 'user_has_posted': true,
|
|
# 'root_discussion_entries': {
|
|
# ...
|
|
# }
|
|
# }
|
|
#
|
|
# For DiscussionTopic, the message is truncated at 4kb.
|
|
#
|
|
# Announcement:
|
|
#
|
|
# !!!javascript
|
|
# {
|
|
# 'type': 'Announcement',
|
|
# 'announcement_id': 1234,
|
|
# 'total_root_discussion_entries': 5,
|
|
# 'require_initial_post': true,
|
|
# 'user_has_posted': null,
|
|
# 'root_discussion_entries': {
|
|
# ...
|
|
# }
|
|
# }
|
|
#
|
|
# For Announcement, the message is truncated at 4kb.
|
|
#
|
|
# Conversation:
|
|
#
|
|
# !!!javascript
|
|
# {
|
|
# 'type': 'Conversation',
|
|
# 'conversation_id': 1234,
|
|
# 'private': false,
|
|
# 'participant_count': 3,
|
|
# }
|
|
#
|
|
# Message:
|
|
#
|
|
# !!!javascript
|
|
# {
|
|
# 'type': 'Message',
|
|
# 'message_id': 1234,
|
|
# 'notification_category': 'Assignment Graded'
|
|
# }
|
|
#
|
|
# Submission:
|
|
#
|
|
# Returns an {api:Submissions:Submission Submission} with its Course and Assignment data.
|
|
#
|
|
# Conference:
|
|
#
|
|
# !!!javascript
|
|
# {
|
|
# 'type': 'Conference',
|
|
# 'web_conference_id': 1234
|
|
# }
|
|
#
|
|
# Collaboration:
|
|
#
|
|
# !!!javascript
|
|
# {
|
|
# 'type': 'Collaboration',
|
|
# 'collaboration_id': 1234
|
|
# }
|
|
#
|
|
# AssessmentRequest:
|
|
#
|
|
# !!!javascript
|
|
# {
|
|
# 'type': 'AssessmentRequest',
|
|
# 'assessment_request_id': 1234
|
|
# }
|
|
def activity_stream
|
|
if @current_user
|
|
# this endpoint has undocumented params (context_code, submission_user_id and asset_type) to
|
|
# support submission comments in the conversations inbox.
|
|
# please replace this with a more reasonable solution at your earliest convenience
|
|
opts = {paginate_url: :api_v1_user_activity_stream_url}
|
|
opts[:asset_type] = params[:asset_type] if params.has_key?(:asset_type)
|
|
opts[:context] = Context.find_by_asset_string(params[:context_code]) if params[:context_code]
|
|
opts[:submission_user_id] = params[:submission_user_id] if params.has_key?(:submission_user_id)
|
|
api_render_stream(opts)
|
|
else
|
|
render_unauthorized_action
|
|
end
|
|
end
|
|
|
|
# @API Activity stream summary
|
|
# Returns a summary of the current user's global activity stream.
|
|
#
|
|
# @example_response
|
|
# [
|
|
# {
|
|
# "type": "DiscussionTopic",
|
|
# "unread_count": 2,
|
|
# "count": 7
|
|
# },
|
|
# {
|
|
# "type": "Conversation",
|
|
# "unread_count": 0,
|
|
# "count": 3
|
|
# }
|
|
# ]
|
|
def activity_stream_summary
|
|
if @current_user
|
|
api_render_stream_summary
|
|
else
|
|
render_unauthorize_action
|
|
end
|
|
end
|
|
|
|
def manageable_courses
|
|
get_context
|
|
return unless authorized_action(@context, @current_user, :manage)
|
|
|
|
# include concluded enrollments as well as active ones if requested
|
|
include_concluded = params[:include].try(:include?, 'concluded')
|
|
@query = params[:course].try(:[], :name) || params[:term]
|
|
@courses = @query.present? ?
|
|
@context.manageable_courses_name_like(@query, include_concluded) :
|
|
@context.manageable_courses(include_concluded).limit(500)
|
|
@courses = @courses.select("courses.*,#{Course.best_unicode_collation_key('name')} AS sort_key").order('sort_key')
|
|
|
|
cancel_cache_buster
|
|
expires_in 30.minutes
|
|
render :json => @courses.map { |c|
|
|
{ :label => c.name, :id => c.id, :term => c.enrollment_term.name,
|
|
:enrollment_start => c.enrollment_term.start_at,
|
|
:account_name => c.enrollment_term.root_account.name,
|
|
:account_id => c.enrollment_term.root_account.id,
|
|
:start_at => datetime_string(c.start_at, :verbose, nil, true),
|
|
:end_at => datetime_string(c.conclude_at, :verbose, nil, true)
|
|
}
|
|
}
|
|
end
|
|
|
|
include Api::V1::TodoItem
|
|
# @API List the TODO items
|
|
# Returns the current user's list of todo items, as seen on the user dashboard.
|
|
#
|
|
# @argument include[] [String, "ungraded_quizzes"]
|
|
# "ungraded_quizzes":: Optionally include ungraded quizzes (such as practice quizzes and surveys) in the list.
|
|
# These will be returned under a +quiz+ key instead of an +assignment+ key in response elements.
|
|
#
|
|
# There is a limit to the number of items returned.
|
|
#
|
|
# The `ignore` and `ignore_permanently` URLs can be used to update the user's
|
|
# preferences on what items will be displayed.
|
|
# Performing a DELETE request against the `ignore` URL will hide that item
|
|
# from future todo item requests, until the item changes.
|
|
# Performing a DELETE request against the `ignore_permanently` URL will hide
|
|
# that item forever.
|
|
#
|
|
# @example_response
|
|
# [
|
|
# {
|
|
# 'type': 'grading', // an assignment that needs grading
|
|
# 'assignment': { .. assignment object .. },
|
|
# 'ignore': '.. url ..',
|
|
# 'ignore_permanently': '.. url ..',
|
|
# 'html_url': '.. url ..',
|
|
# 'needs_grading_count': 3, // number of submissions that need grading
|
|
# 'context_type': 'course', // course|group
|
|
# 'course_id': 1,
|
|
# 'group_id': null,
|
|
# },
|
|
# {
|
|
# 'type' => 'submitting', // an assignment that needs submitting soon
|
|
# 'assignment' => { .. assignment object .. },
|
|
# 'ignore' => '.. url ..',
|
|
# 'ignore_permanently' => '.. url ..',
|
|
# 'html_url': '.. url ..',
|
|
# 'context_type': 'course',
|
|
# 'course_id': 1,
|
|
# },
|
|
# {
|
|
# 'type' => 'submitting', // a quiz that needs submitting soon
|
|
# 'quiz' => { .. quiz object .. },
|
|
# 'ignore' => '.. url ..',
|
|
# 'ignore_permanently' => '.. url ..',
|
|
# 'html_url': '.. url ..',
|
|
# 'context_type': 'course',
|
|
# 'course_id': 1,
|
|
# },
|
|
# ]
|
|
def todo_items
|
|
return render_unauthorized_action unless @current_user
|
|
|
|
grading = @current_user.assignments_needing_grading().map { |a| todo_item_json(a, @current_user, session, 'grading') }
|
|
submitting = @current_user.assignments_needing_submitting(include_ungraded: true, limit: ToDoListPresenter::ASSIGNMENT_LIMIT).map { |a|
|
|
todo_item_json(a, @current_user, session, 'submitting')
|
|
}
|
|
if Array(params[:include]).include? 'ungraded_quizzes'
|
|
submitting += @current_user.ungraded_quizzes_needing_submitting.map { |q| todo_item_json(q, @current_user, session, 'submitting') }
|
|
submitting.sort_by! { |j| (j[:assignment] || j[:quiz])[:due_at] || CanvasSort::Last }
|
|
end
|
|
render :json => (grading + submitting)
|
|
end
|
|
|
|
include Api::V1::Assignment
|
|
include Api::V1::CalendarEvent
|
|
|
|
# @API List upcoming assignments, calendar events
|
|
# Returns the current user's upcoming events, i.e. the same things shown
|
|
# in the dashboard 'Coming Up' sidebar.
|
|
#
|
|
# @example_response
|
|
# [
|
|
# {
|
|
# "id"=>597,
|
|
# "title"=>"Upcoming Course Event",
|
|
# "description"=>"Attendance is correlated with passing!",
|
|
# "start_at"=>"2013-04-27T14:33:14Z",
|
|
# "end_at"=>"2013-04-27T14:33:14Z",
|
|
# "location_name"=>"Red brick house",
|
|
# "location_address"=>"110 Top of the Hill Dr.",
|
|
# "all_day"=>false,
|
|
# "all_day_date"=>nil,
|
|
# "created_at"=>"2013-04-26T14:33:14Z",
|
|
# "updated_at"=>"2013-04-26T14:33:14Z",
|
|
# "workflow_state"=>"active",
|
|
# "context_code"=>"course_12938",
|
|
# "child_events_count"=>0,
|
|
# "child_events"=>[],
|
|
# "parent_event_id"=>nil,
|
|
# "hidden"=>false,
|
|
# "url"=>"http://www.example.com/api/v1/calendar_events/597",
|
|
# "html_url"=>"http://www.example.com/calendar?event_id=597&include_contexts=course_12938"
|
|
# },
|
|
# {
|
|
# "id"=>"assignment_9729",
|
|
# "title"=>"Upcoming Assignment",
|
|
# "description"=>nil,
|
|
# "start_at"=>"2013-04-28T14:47:32Z",
|
|
# "end_at"=>"2013-04-28T14:47:32Z",
|
|
# "all_day"=>false,
|
|
# "all_day_date"=>"2013-04-28",
|
|
# "created_at"=>"2013-04-26T14:47:32Z",
|
|
# "updated_at"=>"2013-04-26T14:47:32Z",
|
|
# "workflow_state"=>"published",
|
|
# "context_code"=>"course_12942",
|
|
# "assignment"=>{
|
|
# "id"=>9729,
|
|
# "name"=>"Upcoming Assignment",
|
|
# "description"=>nil,
|
|
# "points_possible"=>10,
|
|
# "due_at"=>"2013-04-28T14:47:32Z",
|
|
# "assignment_group_id"=>2439,
|
|
# "automatic_peer_reviews"=>false,
|
|
# "grade_group_students_individually"=>nil,
|
|
# "grading_standard_id"=>nil,
|
|
# "grading_type"=>"points",
|
|
# "group_category_id"=>nil,
|
|
# "lock_at"=>nil,
|
|
# "peer_reviews"=>false,
|
|
# "position"=>1,
|
|
# "unlock_at"=>nil,
|
|
# "course_id"=>12942,
|
|
# "submission_types"=>["none"],
|
|
# "muted"=>false,
|
|
# "needs_grading_count"=>0,
|
|
# "html_url"=>"http://www.example.com/courses/12942/assignments/9729"
|
|
# },
|
|
# "url"=>"http://www.example.com/api/v1/calendar_events/assignment_9729",
|
|
# "html_url"=>"http://www.example.com/courses/12942/assignments/9729"
|
|
# }
|
|
# ]
|
|
def upcoming_events
|
|
return render_unauthorized_action unless @current_user
|
|
|
|
Shackles.activate(:slave) do
|
|
prepare_current_user_dashboard_items
|
|
|
|
events = @upcoming_events.map do |e|
|
|
event_json(e, @current_user, session)
|
|
end
|
|
|
|
render :json => events
|
|
end
|
|
end
|
|
|
|
# @API List Missing Submissions
|
|
# returns past-due assignments for which the student does not have a submission.
|
|
# The user sending the request must either be an admin or a parent observer using the parent app
|
|
#
|
|
# @argument user_id
|
|
# the student's ID
|
|
#
|
|
# @returns [Assignment]
|
|
def missing_submissions
|
|
user = api_find(User, params[:user_id])
|
|
return render_unauthorized_action unless @current_user && user.grants_right?(@current_user, :read)
|
|
|
|
assignments = []
|
|
Shackles.activate(:slave) do
|
|
preloaded_submitted_assignment_ids = user.submissions.not_missing.pluck(:assignment_id)
|
|
assignments = user.assignments_needing_submitting due_before: Time.zone.now
|
|
assignments.reject {|as| preloaded_submitted_assignment_ids.include? as.id }
|
|
end
|
|
|
|
render json: assignments.map {|as| assignment_json(as, user, session) }
|
|
end
|
|
|
|
def ignore_item
|
|
unless %w[grading submitting reviewing moderation].include?(params[:purpose])
|
|
return render(:json => { :ignored => false }, :status => 400)
|
|
end
|
|
@current_user.ignore_item!(ActiveRecord::Base.find_by_asset_string(params[:asset_string], ['Assignment', 'AssessmentRequest', 'Quizzes::Quiz']),
|
|
params[:purpose], params[:permanent] == '1')
|
|
render :json => { :ignored => true }
|
|
end
|
|
|
|
# @API Hide a stream item
|
|
# Hide the given stream item.
|
|
#
|
|
# @example_request
|
|
# curl https://<canvas>/api/v1/users/self/activity_stream/<stream_item_id> \
|
|
# -X DELETE \
|
|
# -H 'Authorization: Bearer <token>'
|
|
#
|
|
# @example_response
|
|
# {
|
|
# "hidden": true
|
|
# }
|
|
def ignore_stream_item
|
|
@current_user.shard.activate do # can't just pass in the user's shard to relative_id_for, since local ids will be incorrectly scoped to the current shard, not the user's
|
|
if item = @current_user.stream_item_instances.where(stream_item_id: Shard.relative_id_for(params[:id], Shard.current, Shard.current)).first
|
|
item.update_attribute(:hidden, true) # observer handles cache invalidation
|
|
end
|
|
end
|
|
render :json => { :hidden => true }
|
|
end
|
|
|
|
# @API Hide all stream items
|
|
# Hide all stream items for the user
|
|
#
|
|
# @example_request
|
|
# curl https://<canvas>/api/v1/users/self/activity_stream \
|
|
# -X DELETE \
|
|
# -H 'Authorization: Bearer <token>'
|
|
#
|
|
# @example_response
|
|
# {
|
|
# "hidden": true
|
|
# }
|
|
def ignore_all_stream_items
|
|
@current_user.shard.activate do # can't just pass in the user's shard to relative_id_for, since local ids will be incorrectly scoped to the current shard, not the user's
|
|
@current_user.stream_item_instances.where(:hidden => false).each do |item|
|
|
item.update_attribute(:hidden, true) # observer handles cache invalidation
|
|
end
|
|
end
|
|
render :json => { :hidden => true }
|
|
end
|
|
|
|
# @API Upload a file
|
|
#
|
|
# Upload a file to the user's personal files section.
|
|
#
|
|
# This API endpoint is the first step in uploading a file to a user's files.
|
|
# See the {file:file_uploads.html File Upload Documentation} for details on
|
|
# the file upload workflow.
|
|
#
|
|
# Note that typically users will only be able to upload files to their
|
|
# own files section. Passing a user_id of +self+ is an easy shortcut
|
|
# to specify the current user.
|
|
def create_file
|
|
@user = api_find(User, params[:user_id])
|
|
@attachment = @user.attachments.build
|
|
if authorized_action(@attachment, @current_user, :create)
|
|
@context = @user
|
|
api_attachment_preflight(@current_user, request, :check_quota => true)
|
|
end
|
|
end
|
|
|
|
def close_notification
|
|
@current_user.close_announcement(AccountNotification.find(params[:id]))
|
|
render :json => @current_user
|
|
end
|
|
|
|
def delete_user_service
|
|
deleted = @current_user.user_services.find(params[:id]).destroy
|
|
if deleted.service == "google_drive"
|
|
Rails.cache.delete(['google_drive_tokens', @current_user].cache_key)
|
|
end
|
|
render :json => {:deleted => true}
|
|
end
|
|
|
|
ServiceCredentials = Struct.new(:service_user_name,:decrypted_password)
|
|
|
|
def create_user_service
|
|
begin
|
|
user_name = params[:user_service][:user_name]
|
|
password = params[:user_service][:password]
|
|
service = ServiceCredentials.new( user_name, password )
|
|
case params[:user_service][:service]
|
|
when 'delicious'
|
|
delicious_get_last_posted(service)
|
|
when 'diigo'
|
|
Diigo::Connection.diigo_get_bookmarks(service)
|
|
when 'skype'
|
|
true
|
|
when 'yo'
|
|
true
|
|
else
|
|
raise "Unknown Service"
|
|
end
|
|
@service = UserService.register_from_params(@current_user, params[:user_service])
|
|
render :json => @service
|
|
rescue => e
|
|
render :json => {:errors => true}, :status => :bad_request
|
|
end
|
|
end
|
|
|
|
def services
|
|
params[:service_types] ||= params[:service_type]
|
|
json = Rails.cache.fetch(['user_services', @current_user, params[:service_type]].cache_key) do
|
|
@services = @current_user.user_services rescue []
|
|
if params[:service_types]
|
|
@services = @services.of_type(params[:service_types].split(",")) rescue []
|
|
end
|
|
@services.map{ |s| s.as_json(only: [:service_user_id, :service_user_url, :service_user_name, :service, :type, :id]) }
|
|
end
|
|
render :json => json
|
|
end
|
|
|
|
def bookmark_search
|
|
@service = @current_user.user_services.where(type: 'BookmarkService', service: params[:service_type]).first rescue nil
|
|
res = nil
|
|
res = @service.find_bookmarks(params[:q]) if @service
|
|
render :json => res
|
|
end
|
|
|
|
def show
|
|
get_context
|
|
@context_account = @context.is_a?(Account) ? @context : @domain_root_account
|
|
@user = params[:id] && params[:id] != 'self' ? User.find(params[:id]) : @current_user
|
|
if authorized_action(@user, @current_user, :read_full_profile)
|
|
add_crumb(t('crumbs.profile', "%{user}'s profile", :user => @user.short_name), @user == @current_user ? user_profile_path(@current_user) : user_path(@user) )
|
|
|
|
@group_memberships = @user.current_group_memberships
|
|
|
|
# course_section and enrollment term will only be used if the enrollment dates haven't been cached yet;
|
|
# maybe should just look at the first enrollment and check if it's cached to decide if we should include
|
|
# them here
|
|
@enrollments = @user.enrollments.
|
|
shard(@user).
|
|
where("enrollments.workflow_state<>'deleted' AND courses.workflow_state<>'deleted'").
|
|
eager_load(:course).
|
|
preload(:associated_user, :course_section, :enrollment_state, course: { enrollment_term: :enrollment_dates_overrides }).to_a
|
|
|
|
# restrict view for other users
|
|
if @user != @current_user
|
|
@enrollments = @enrollments.select{|e| e.grants_right?(@current_user, session, :read)}
|
|
end
|
|
|
|
@enrollments = @enrollments.sort_by {|e| [e.state_sortable, e.rank_sortable, e.course.name] }
|
|
# pre-populate the reverse association
|
|
@enrollments.each { |e| e.user = @user }
|
|
|
|
respond_to do |format|
|
|
format.html
|
|
format.json {
|
|
render :json => user_json(@user, @current_user, session, %w{locale avatar_url},
|
|
@current_user.pseudonym.account) }
|
|
end
|
|
end
|
|
end
|
|
|
|
# @API Show user details
|
|
#
|
|
# Shows details for user.
|
|
#
|
|
# Also includes an attribute "permissions", a non-comprehensive list of permissions for the user.
|
|
# Example:
|
|
# !!!javascript
|
|
# "permissions": {
|
|
# "can_update_name": true, // Whether the user can update their name.
|
|
# "can_update_avatar": false // Whether the user can update their avatar.
|
|
# }
|
|
#
|
|
# @example_request
|
|
# curl https://<canvas>/api/v1/users/self \
|
|
# -X GET \
|
|
# -H 'Authorization: Bearer <token>'
|
|
#
|
|
# @returns User
|
|
def api_show
|
|
@user = api_find(User, params[:id])
|
|
if @user.grants_any_right?(@current_user, session, :manage, :manage_user_details)
|
|
render :json => user_json(@user, @current_user, session, %w{locale avatar_url permissions}, @current_user.pseudonym.account)
|
|
else
|
|
render_unauthorized_action
|
|
end
|
|
end
|
|
|
|
def external_tool
|
|
@tool = ContextExternalTool.find_for(params[:id], @domain_root_account, :user_navigation)
|
|
@opaque_id = @tool.opaque_identifier_for(@current_user)
|
|
@resource_type = 'user_navigation'
|
|
|
|
success_url = user_profile_url(@current_user)
|
|
@return_url = named_context_url(@current_user, :context_external_content_success_url, 'external_tool_redirect', {include_host: true})
|
|
@redirect_return = true
|
|
js_env(:redirect_return_success_url => success_url,
|
|
:redirect_return_cancel_url => success_url)
|
|
|
|
@lti_launch = @tool.settings['post_only'] ? Lti::Launch.new(post_only: true) : Lti::Launch.new
|
|
opts = {
|
|
resource_type: @resource_type,
|
|
link_code: @opaque_id
|
|
}
|
|
variable_expander = Lti::VariableExpander.new(@domain_root_account, @context, self,{
|
|
current_user: @current_user,
|
|
current_pseudonym: @current_pseudonym,
|
|
tool: @tool})
|
|
adapter = Lti::LtiOutboundAdapter.new(@tool, @current_user, @domain_root_account).prepare_tool_launch(@return_url, variable_expander, opts)
|
|
@lti_launch.params = adapter.generate_post_payload
|
|
|
|
@lti_launch.resource_url = @tool.user_navigation(:url)
|
|
@lti_launch.link_text = @tool.label_for(:user_navigation, I18n.locale)
|
|
@lti_launch.analytics_id = @tool.tool_id
|
|
|
|
@active_tab = @tool.asset_string
|
|
add_crumb(@current_user.short_name, user_profile_path(@current_user))
|
|
render Lti::AppUtil.display_template
|
|
end
|
|
|
|
def new
|
|
return redirect_to(root_url) if @current_user
|
|
run_login_hooks
|
|
js_env :ACCOUNT => account_json(@domain_root_account, nil, session, ['registration_settings']),
|
|
:PASSWORD_POLICY => @domain_root_account.password_policy
|
|
render :layout => 'bare'
|
|
end
|
|
|
|
include Api::V1::User
|
|
include Api::V1::Avatar
|
|
include Api::V1::Account
|
|
|
|
# @API Create a user
|
|
# Create and return a new user and pseudonym for an account.
|
|
#
|
|
# If you don't have the "Modify login details for users" permission, but
|
|
# self-registration is enabled on the account, you can still use this
|
|
# endpoint to register new users. Certain fields will be required, and
|
|
# others will be ignored (see below).
|
|
#
|
|
# @argument user[name] [String]
|
|
# The full name of the user. This name will be used by teacher for grading.
|
|
# Required if this is a self-registration.
|
|
#
|
|
# @argument user[short_name] [String]
|
|
# User's name as it will be displayed in discussions, messages, and comments.
|
|
#
|
|
# @argument user[sortable_name] [String]
|
|
# User's name as used to sort alphabetically in lists.
|
|
#
|
|
# @argument user[time_zone] [String]
|
|
# The time zone for the user. Allowed time zones are
|
|
# {http://www.iana.org/time-zones IANA time zones} or friendlier
|
|
# {http://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html Ruby on Rails time zones}.
|
|
#
|
|
# @argument user[locale] [String]
|
|
# The user's preferred language, from the list of languages Canvas supports.
|
|
# This is in RFC-5646 format.
|
|
#
|
|
# @argument user[birthdate] [Date]
|
|
# The user's birth date.
|
|
#
|
|
# @argument user[terms_of_use] [Boolean]
|
|
# Whether the user accepts the terms of use. Required if this is a
|
|
# self-registration and this canvas instance requires users to accept
|
|
# the terms (on by default).
|
|
#
|
|
# If this is true, it will mark the user as having accepted the terms of use.
|
|
#
|
|
# @argument user[skip_registration] [Boolean]
|
|
# Automatically mark the user as registered.
|
|
#
|
|
# If this is true, it is recommended to set <tt>"pseudonym[send_confirmation]"</tt> to true as well.
|
|
# Otherwise, the user will not receive any messages about their account creation.
|
|
#
|
|
# The users communication channel confirmation can be skipped by setting
|
|
# <tt>"communication_channel[skip_confirmation]"</tt> to true as well.
|
|
#
|
|
# @argument pseudonym[unique_id] [Required, String]
|
|
# User's login ID. If this is a self-registration, it must be a valid
|
|
# email address.
|
|
#
|
|
# @argument pseudonym[password] [String]
|
|
# User's password. Cannot be set during self-registration.
|
|
#
|
|
# @argument pseudonym[sis_user_id] [String]
|
|
# SIS ID for the user's account. To set this parameter, the caller must be
|
|
# able to manage SIS permissions.
|
|
#
|
|
# @argument pseudonym[integration_id] [String]
|
|
# Integration ID for the login. To set this parameter, the caller must be able to
|
|
# manage SIS permissions. The Integration ID is a secondary
|
|
# identifier useful for more complex SIS integrations.
|
|
#
|
|
# @argument pseudonym[send_confirmation] [Boolean]
|
|
# Send user notification of account creation if true.
|
|
# Automatically set to true during self-registration.
|
|
#
|
|
# @argument pseudonym[force_self_registration] [Boolean]
|
|
# Send user a self-registration style email if true.
|
|
# Setting it means the users will get a notification asking them
|
|
# to "complete the registration process" by clicking it, setting
|
|
# a password, and letting them in. Will only be executed on
|
|
# if the user does not need admin approval.
|
|
# Defaults to false unless explicitly provided.
|
|
#
|
|
# @argument pseudonym[authentication_provider_id] [String]
|
|
# The authentication provider this login is associated with. Logins
|
|
# associated with a specific provider can only be used with that provider.
|
|
# Legacy providers (LDAP, CAS, SAML) will search for logins associated with
|
|
# them, or unassociated logins. New providers will only search for logins
|
|
# explicitly associated with them. This can be the integer ID of the
|
|
# provider, or the type of the provider (in which case, it will find the
|
|
# first matching provider).
|
|
#
|
|
# @argument communication_channel[type] [String]
|
|
# The communication channel type, e.g. 'email' or 'sms'.
|
|
#
|
|
# @argument communication_channel[address] [String]
|
|
# The communication channel address, e.g. the user's email address.
|
|
#
|
|
# @argument communication_channel[confirmation_url] [Boolean]
|
|
# Only valid for account admins. If true, returns the new user account
|
|
# confirmation URL in the response.
|
|
#
|
|
# @argument communication_channel[skip_confirmation] [Boolean]
|
|
# Only valid for site admins and account admins making requests; If true, the channel is
|
|
# automatically validated and no confirmation email or SMS is sent.
|
|
# Otherwise, the user must respond to a confirmation message to confirm the
|
|
# channel.
|
|
#
|
|
# If this is true, it is recommended to set <tt>"pseudonym[send_confirmation]"</tt> to true as well.
|
|
# Otherwise, the user will not receive any messages about their account creation.
|
|
#
|
|
# @argument force_validations [Boolean]
|
|
# If true, validations are performed on the newly created user (and their associated pseudonym)
|
|
# even if the request is made by a privileged user like an admin. When set to false,
|
|
# or not included in the request parameters, any newly created users are subject to
|
|
# validations unless the request is made by a user with a 'manage_user_logins' right.
|
|
# In which case, certain validations such as 'require_acceptance_of_terms' and
|
|
# 'require_presence_of_name' are not enforced. Use this parameter to return helpful json
|
|
# errors while building users with an admin request.
|
|
#
|
|
# @argument enable_sis_reactivation [Boolean]
|
|
# When true, will first try to re-activate a deleted user with matching sis_user_id if possible.
|
|
#
|
|
# @returns User
|
|
def create
|
|
create_user
|
|
end
|
|
|
|
# @API Self register a user
|
|
# Self register and return a new user and pseudonym for an account.
|
|
#
|
|
# If self-registration is enabled on the account, you can use this
|
|
# endpoint to self register new users.
|
|
#
|
|
# @argument user[name] [Required, String]
|
|
# The full name of the user. This name will be used by teacher for grading.
|
|
#
|
|
#
|
|
# @argument user[short_name] [String]
|
|
# User's name as it will be displayed in discussions, messages, and comments.
|
|
#
|
|
# @argument user[sortable_name] [String]
|
|
# User's name as used to sort alphabetically in lists.
|
|
#
|
|
# @argument user[time_zone] [String]
|
|
# The time zone for the user. Allowed time zones are
|
|
# {http://www.iana.org/time-zones IANA time zones} or friendlier
|
|
# {http://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html Ruby on Rails time zones}.
|
|
#
|
|
# @argument user[locale] [String]
|
|
# The user's preferred language, from the list of languages Canvas supports.
|
|
# This is in RFC-5646 format.
|
|
#
|
|
# @argument user[birthdate] [Date]
|
|
# The user's birth date.
|
|
#
|
|
# @argument user[terms_of_use] [Required, Boolean]
|
|
# Whether the user accepts the terms of use.
|
|
#
|
|
# @argument pseudonym[unique_id] [Required, String]
|
|
# User's login ID. Must be a valid email address.
|
|
#
|
|
# @argument communication_channel[type] [String]
|
|
# The communication channel type, e.g. 'email' or 'sms'.
|
|
#
|
|
# @argument communication_channel[address] [String]
|
|
# The communication channel address, e.g. the user's email address.
|
|
#
|
|
# @returns User
|
|
def create_self_registered_user
|
|
create_user
|
|
end
|
|
|
|
# @API Update user settings.
|
|
# Update an existing user's settings.
|
|
#
|
|
# @argument manual_mark_as_read [Boolean]
|
|
# If true, require user to manually mark discussion posts as read (don't
|
|
# auto-mark as read).
|
|
#
|
|
# @argument collapse_global_nav [Boolean]
|
|
# If true, the user's page loads with the global navigation collapsed
|
|
#
|
|
# @example_request
|
|
#
|
|
# curl 'https://<canvas>/api/v1/users/<user_id>/settings \
|
|
# -X PUT \
|
|
# -F 'manual_mark_as_read=true'
|
|
# -H 'Authorization: Bearer <token>'
|
|
def settings
|
|
user = api_find(User, params[:id])
|
|
|
|
case
|
|
when request.get?
|
|
return unless authorized_action(user, @current_user, :read)
|
|
render(json: {
|
|
manual_mark_as_read: @current_user.manual_mark_as_read?,
|
|
collapse_global_nav: @current_user.collapse_global_nav?
|
|
})
|
|
when request.put?
|
|
return unless authorized_action(user, @current_user, [:manage, :manage_user_details])
|
|
unless params[:manual_mark_as_read].nil?
|
|
mark_as_read = value_to_boolean(params[:manual_mark_as_read])
|
|
user.preferences[:manual_mark_as_read] = mark_as_read
|
|
end
|
|
unless params[:collapse_global_nav].nil?
|
|
collapse_global_nav = value_to_boolean(params[:collapse_global_nav])
|
|
user.preferences[:collapse_global_nav] = collapse_global_nav
|
|
end
|
|
|
|
respond_to do |format|
|
|
format.json {
|
|
if user.save
|
|
render(json: {
|
|
manual_mark_as_read: user.manual_mark_as_read?,
|
|
collapse_global_nav: user.collapse_global_nav?
|
|
})
|
|
else
|
|
render(json: user.errors, status: :bad_request)
|
|
end
|
|
}
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
def get_new_user_tutorial_statuses
|
|
user = api_find(User, params[:id])
|
|
unless user == @current_user
|
|
return render(json: { :message => "This endpoint only works against the current user" }, status: :unauthorized)
|
|
end
|
|
return unless authorized_action(user, @current_user, :manage)
|
|
render_new_user_tutorial_statuses(user)
|
|
end
|
|
|
|
def set_new_user_tutorial_status
|
|
user = api_find(User, params[:id])
|
|
unless user == @current_user
|
|
return render(json: { :message => "This endpoint only works against the current user" }, status: :unauthorized)
|
|
end
|
|
|
|
valid_names = %w{home modules pages assignments quizzes settings files people announcements
|
|
grades discussions syllabus collaborations import conferences}
|
|
|
|
# Check if the page_name is valid
|
|
unless valid_names.include?(params[:page_name])
|
|
return render(json: { :message => "Invalid Page Name Provided" }, status: :bad_request)
|
|
end
|
|
|
|
user.new_user_tutorial_statuses[params[:page_name]] = value_to_boolean(params[:collapsed])
|
|
|
|
respond_to do |format|
|
|
format.json do
|
|
if user.save
|
|
render_new_user_tutorial_statuses(user)
|
|
else
|
|
render(json: user.errors, status: :bad_request)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
# @API Get custom colors
|
|
# Returns all custom colors that have been saved for a user.
|
|
#
|
|
# @example_request
|
|
#
|
|
# curl 'https://<canvas>/api/v1/users/<user_id>/colors/ \
|
|
# -X GET \
|
|
# -H 'Authorization: Bearer <token>'
|
|
#
|
|
# @example_response
|
|
# {
|
|
# "custom_colors": {
|
|
# "course_42": "#abc123",
|
|
# "course_88": "#123abc"
|
|
# }
|
|
# }
|
|
#
|
|
def get_custom_colors
|
|
user = api_find(User, params[:id])
|
|
return unless authorized_action(user, @current_user, :read)
|
|
render(json: {custom_colors: user.custom_colors})
|
|
end
|
|
|
|
# @API Get custom color
|
|
# Returns the custom colors that have been saved for a user for a given context.
|
|
#
|
|
# The asset_string parameter should be in the format 'context_id', for example
|
|
# 'course_42'.
|
|
#
|
|
# @example_request
|
|
#
|
|
# curl 'https://<canvas>/api/v1/users/<user_id>/colors/<asset_string> \
|
|
# -X GET \
|
|
# -H 'Authorization: Bearer <token>'
|
|
#
|
|
# @example_response
|
|
# {
|
|
# "hexcode": "#abc123"
|
|
# }
|
|
def get_custom_color
|
|
user = api_find(User, params[:id])
|
|
|
|
return unless authorized_action(user, @current_user, :read)
|
|
|
|
if user.custom_colors[params[:asset_string]].nil?
|
|
raise(ActiveRecord::RecordNotFound, "Asset does not have an associated color.")
|
|
end
|
|
render(json: { hexcode: user.custom_colors[params[:asset_string]]})
|
|
end
|
|
|
|
|
|
|
|
# @API Update custom color
|
|
# Updates a custom color for a user for a given context. This allows
|
|
# colors for the calendar and elsewhere to be customized on a user basis.
|
|
#
|
|
# The asset string parameter should be in the format 'context_id', for example
|
|
# 'course_42'
|
|
#
|
|
# @argument hexcode [String]
|
|
# The hexcode of the color to set for the context, if you choose to pass the
|
|
# hexcode as a query parameter rather than in the request body you should
|
|
# NOT include the '#' unless you escape it first.
|
|
#
|
|
# @example_request
|
|
#
|
|
# curl 'https://<canvas>/api/v1/users/<user_id>/colors/<asset_string> \
|
|
# -X PUT \
|
|
# -F 'hexcode=fffeee'
|
|
# -H 'Authorization: Bearer <token>'
|
|
#
|
|
# @example_response
|
|
# {
|
|
# "hexcode": "#abc123"
|
|
# }
|
|
def set_custom_color
|
|
user = api_find(User, params[:id])
|
|
|
|
return unless authorized_action(user, @current_user, [:manage, :manage_user_details])
|
|
|
|
# Make sure the user has rights to the actual context used.
|
|
context = Context.find_by_asset_string(params[:asset_string])
|
|
|
|
if context.nil?
|
|
raise(ActiveRecord::RecordNotFound, "Asset does not exist")
|
|
end
|
|
|
|
return unless authorized_action(context, @current_user, :read)
|
|
|
|
# Check if the hexcode is valid
|
|
unless valid_hexcode?(params[:hexcode])
|
|
return render(json: { :message => "Invalid Hexcode Provided" }, status: :bad_request)
|
|
end
|
|
|
|
unless params[:hexcode].nil?
|
|
user.custom_colors[params[:asset_string]] = normalize_hexcode(params[:hexcode])
|
|
end
|
|
|
|
respond_to do |format|
|
|
format.json do
|
|
if user.save
|
|
render(json: { hexcode: user.custom_colors[params[:asset_string]]})
|
|
else
|
|
render(json: user.errors, status: :bad_request)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
# @API Get dashboard postions
|
|
# Returns all dashboard positions that have been saved for a user.
|
|
#
|
|
# @example_request
|
|
#
|
|
# curl 'https://<canvas>/api/v1/users/<user_id>/dashboard_positions/ \
|
|
# -X GET \
|
|
# -H 'Authorization: Bearer <token>'
|
|
#
|
|
# @example_response
|
|
# {
|
|
# "dashboard_positions": {
|
|
# "course_42": 2,
|
|
# "course_88": 1
|
|
# }
|
|
# }
|
|
#
|
|
def get_dashboard_positions
|
|
user = api_find(User, params[:id])
|
|
return unless authorized_action(user, @current_user, :read)
|
|
render(json: {dashboard_positions: user.dashboard_positions})
|
|
end
|
|
|
|
# @API Update dashboard positions
|
|
# Updates the dashboard positions for a user for a given context. This allows
|
|
# positions for the dashboard cards and elsewhere to be customized on a per
|
|
# user basis.
|
|
#
|
|
# The asset string parameter should be in the format 'context_id', for example
|
|
# 'course_42'
|
|
#
|
|
# @example_request
|
|
#
|
|
# curl 'https://<canvas>/api/v1/users/<user_id>/dashboard_positions/ \
|
|
# -X PUT \
|
|
# -F 'dashboard_positions[course_42]=1' \
|
|
# -F 'dashboard_positions[course_53]=2' \
|
|
# -F 'dashboard_positions[course_10]=3' \
|
|
# -H 'Authorization: Bearer <token>'
|
|
#
|
|
# @example_response
|
|
# {
|
|
# "dashboard_positions": {
|
|
# "course_10": 3,
|
|
# "course_42": 1,
|
|
# "course_53": 2
|
|
# }
|
|
# }
|
|
def set_dashboard_positions
|
|
user = api_find(User, params[:id])
|
|
|
|
return unless authorized_action(user, @current_user, [:manage, :manage_user_details])
|
|
params[:dashboard_positions].each do |key, val|
|
|
context = Context.find_by_asset_string(key)
|
|
if context.nil?
|
|
raise(ActiveRecord::RecordNotFound, "Asset #{key} does not exist")
|
|
end
|
|
return unless authorized_action(context, @current_user, :read)
|
|
position = Integer(val) rescue nil
|
|
if position.nil?
|
|
return render(json: { :message => "Invalid position provided" }, status: :bad_request)
|
|
end
|
|
end
|
|
|
|
user.dashboard_positions = user.dashboard_positions.merge(params[:dashboard_positions])
|
|
|
|
respond_to do |format|
|
|
format.json do
|
|
if user.save
|
|
render(json: { dashboard_positions: user.dashboard_positions })
|
|
else
|
|
render(json: user.errors, status: :bad_request)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
# @API Edit a user
|
|
# Modify an existing user. To modify a user's login, see the documentation for logins.
|
|
#
|
|
# @argument user[name] [String]
|
|
# The full name of the user. This name will be used by teacher for grading.
|
|
#
|
|
# @argument user[short_name] [String]
|
|
# User's name as it will be displayed in discussions, messages, and comments.
|
|
#
|
|
# @argument user[sortable_name] [String]
|
|
# User's name as used to sort alphabetically in lists.
|
|
#
|
|
# @argument user[time_zone] [String]
|
|
# The time zone for the user. Allowed time zones are
|
|
# {http://www.iana.org/time-zones IANA time zones} or friendlier
|
|
# {http://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html Ruby on Rails time zones}.
|
|
#
|
|
# @argument user[email] [String]
|
|
# The default email address of the user.
|
|
#
|
|
# @argument user[locale] [String]
|
|
# The user's preferred language, from the list of languages Canvas supports.
|
|
# This is in RFC-5646 format.
|
|
#
|
|
# @argument user[avatar][token] [String]
|
|
# A unique representation of the avatar record to assign as the user's
|
|
# current avatar. This token can be obtained from the user avatars endpoint.
|
|
# This supersedes the user [avatar] [url] argument, and if both are included
|
|
# the url will be ignored. Note: this is an internal representation and is
|
|
# subject to change without notice. It should be consumed with this api
|
|
# endpoint and used in the user update endpoint, and should not be
|
|
# constructed by the client.
|
|
#
|
|
# @argument user[avatar][url] [String]
|
|
# To set the user's avatar to point to an external url, do not include a
|
|
# token and instead pass the url here. Warning: For maximum compatibility,
|
|
# please use 128 px square images.
|
|
#
|
|
# @example_request
|
|
#
|
|
# curl 'https://<canvas>/api/v1/users/133.json' \
|
|
# -X PUT \
|
|
# -F 'user[name]=Sheldon Cooper' \
|
|
# -F 'user[short_name]=Shelly' \
|
|
# -F 'user[time_zone]=Pacific Time (US & Canada)' \
|
|
# -F 'user[avatar][token]=<opaque_token>' \
|
|
# -H "Authorization: Bearer <token>"
|
|
#
|
|
# @returns User
|
|
def update
|
|
params[:user] ||= {}
|
|
user_params = params[:user]
|
|
@user = api_request? ?
|
|
api_find(User, params[:id]) :
|
|
params[:id] ? api_find(User, params[:id]) : @current_user
|
|
|
|
if params[:default_pseudonym_id] && authorized_action(@user, @current_user, :manage)
|
|
@default_pseudonym = @user.pseudonyms.find(params[:default_pseudonym_id])
|
|
@default_pseudonym.move_to_top
|
|
end
|
|
|
|
update_email = @user.grants_right?(@current_user, :manage_user_details) && user_params[:email]
|
|
managed_attributes = []
|
|
managed_attributes.concat [:name, :short_name, :sortable_name, :birthdate] if @user.grants_right?(@current_user, :rename)
|
|
managed_attributes << :terms_of_use if @user == (@real_current_user || @current_user)
|
|
managed_attributes << :email if update_email
|
|
|
|
if @user.grants_right?(@current_user, :manage_user_details)
|
|
managed_attributes.concat([:time_zone, :locale])
|
|
end
|
|
|
|
if @user.grants_right?(@current_user, :update_avatar)
|
|
avatar = user_params.delete(:avatar)
|
|
|
|
# delete any avatar_image passed, because we only allow updating avatars
|
|
# based on [:avatar][:token].
|
|
user_params.delete(:avatar_image)
|
|
|
|
managed_attributes << :avatar_image
|
|
if token = avatar.try(:[], :token)
|
|
if av_json = avatar_for_token(@user, token)
|
|
user_params[:avatar_image] = { :type => av_json['type'],
|
|
:url => av_json['url'] }
|
|
end
|
|
elsif url = avatar.try(:[], :url)
|
|
user_params[:avatar_image] = { :type => 'external', :url => url }
|
|
end
|
|
end
|
|
|
|
if managed_attributes.any? && user_params.except(*managed_attributes).empty?
|
|
managed_attributes << {:avatar_image => strong_anything} if managed_attributes.delete(:avatar_image)
|
|
user_params = user_params.permit(*managed_attributes)
|
|
new_email = user_params.delete(:email)
|
|
# admins can update avatar images even if they are locked
|
|
admin_avatar_update = user_params[:avatar_image] &&
|
|
@user.grants_right?(@current_user, :update_avatar) &&
|
|
@user.grants_right?(@current_user, :manage_user_details)
|
|
|
|
if admin_avatar_update
|
|
old_avatar_state = @user.avatar_state
|
|
@user.avatar_state = 'submitted'
|
|
end
|
|
|
|
if session[:require_terms]
|
|
@user.require_acceptance_of_terms = true
|
|
end
|
|
|
|
if user_params[:birthdate].present? && user_params[:birthdate] !~ Api::ISO8601_REGEX &&
|
|
params[:user][:birthdate] !~ Api::DATE_REGEX
|
|
return render(:json => {:errors => {:birthdate => t(:birthdate_invalid,
|
|
'Invalid date or invalid datetime for birthdate')}}, :status => 400)
|
|
end
|
|
|
|
respond_to do |format|
|
|
if @user.update_attributes(user_params)
|
|
@user.avatar_state = (old_avatar_state == :locked ? old_avatar_state : 'approved') if admin_avatar_update
|
|
@user.email = new_email if update_email
|
|
@user.save if admin_avatar_update || update_email
|
|
session.delete(:require_terms)
|
|
flash[:notice] = t('user_updated', 'User was successfully updated.')
|
|
unless params[:redirect_to_previous].blank?
|
|
if CANVAS_RAILS4_2
|
|
return redirect_to :back
|
|
else
|
|
return redirect_back fallback_location: user_url(@user)
|
|
end
|
|
end
|
|
format.html { redirect_to user_url(@user) }
|
|
format.json {
|
|
render :json => user_json(@user, @current_user, session, %w{locale avatar_url email time_zone},
|
|
@current_user.pseudonym.account) }
|
|
else
|
|
format.html { render :edit }
|
|
format.json { render :json => @user.errors, :status => :bad_request }
|
|
end
|
|
end
|
|
else
|
|
render_unauthorized_action
|
|
end
|
|
end
|
|
|
|
def media_download
|
|
fetcher = MediaSourceFetcher.new(CanvasKaltura::ClientV3.new)
|
|
extension = params[:type]
|
|
media_type = params[:media_type]
|
|
extension ||= params[:format] if media_type.nil?
|
|
|
|
url = fetcher.fetch_preferred_source_url(
|
|
media_id: params[:entryId],
|
|
file_extension: extension,
|
|
media_type: media_type
|
|
)
|
|
if url
|
|
if params[:redirect] == '1'
|
|
redirect_to url
|
|
else
|
|
render :json => { 'url' => url }
|
|
end
|
|
else
|
|
render :status => 404, :plain => t('could_not_find_url', "Could not find download URL")
|
|
end
|
|
end
|
|
|
|
def merge
|
|
@source_user = User.find(params[:user_id])
|
|
@target_user = User.where(id: params[:new_user_id]).first if params[:new_user_id]
|
|
@target_user ||= @current_user
|
|
if @source_user.grants_right?(@current_user, :merge) && @target_user.grants_right?(@current_user, :merge)
|
|
UserMerge.from(@source_user).into(@target_user)
|
|
@target_user.touch
|
|
flash[:notice] = t('user_merge_success', "User merge succeeded! %{first_user} and %{second_user} are now one and the same.", :first_user => @target_user.name, :second_user => @source_user.name)
|
|
if @target_user == @current_user
|
|
redirect_to user_profile_url(@current_user)
|
|
else
|
|
redirect_to user_url(@target_user)
|
|
end
|
|
else
|
|
flash[:error] = t('user_merge_fail', "User merge failed. Please make sure you have proper permission and try again.")
|
|
redirect_to dashboard_url
|
|
end
|
|
end
|
|
|
|
def admin_merge
|
|
@user = User.find(params[:user_id])
|
|
if authorized_action(@user, @current_user, :merge)
|
|
if params[:clear]
|
|
params.delete(:new_user_id)
|
|
params.delete(:pending_user_id)
|
|
end
|
|
|
|
if params[:new_user_id].present?
|
|
@other_user = api_find_all(User, [params[:new_user_id]]).first
|
|
if !@other_user || !@other_user.grants_right?(@current_user, :merge)
|
|
@other_user = nil
|
|
flash[:error] = t('user_not_found', "No active user with that ID was found.")
|
|
elsif @other_user == @user
|
|
@other_user = nil
|
|
flash[:error] = t('cant_self_merge', "You can't merge an account with itself.")
|
|
end
|
|
end
|
|
|
|
if params[:pending_user_id].present?
|
|
@pending_other_user = api_find_all(User, [params[:pending_user_id]]).first
|
|
if !@pending_other_user || !@pending_other_user.grants_right?(@current_user, :merge)
|
|
@pending_other_user = nil
|
|
flash[:error] = t('user_not_found', "No active user with that ID was found.")
|
|
elsif @pending_other_user == @user
|
|
@pending_other_user = nil
|
|
flash[:error] = t('cant_self_merge', "You can't merge an account with itself.")
|
|
end
|
|
end
|
|
|
|
render :admin_merge
|
|
end
|
|
end
|
|
|
|
def assignments_needing_grading
|
|
@user = User.find(params[:user_id])
|
|
if authorized_action(@user, @current_user, :read)
|
|
res = @user.assignments_needing_grading
|
|
render :json => res
|
|
end
|
|
end
|
|
|
|
def assignments_needing_submitting
|
|
@user = User.find(params[:user_id])
|
|
if authorized_action(@user, @current_user, :read)
|
|
render :json => @user.assignments_needing_submitting
|
|
end
|
|
end
|
|
|
|
def mark_avatar_image
|
|
if params[:remove]
|
|
if authorized_action(@user, @current_user, :remove_avatar)
|
|
@user.avatar_image = {}
|
|
@user.save
|
|
render :json => @user
|
|
end
|
|
else
|
|
if !session["reported_#{@user.id}".to_sym]
|
|
if params[:context_code]
|
|
@context = Context.find_by_asset_string(params[:context_code]) rescue nil
|
|
@context = nil unless context.respond_to?(:users) && context.users.where(id: @user).first
|
|
end
|
|
@user.report_avatar_image!(@context)
|
|
end
|
|
session["reports_#{@user.id}".to_sym] = true
|
|
render :json => {:reported => true}
|
|
end
|
|
end
|
|
|
|
def report_avatar_image
|
|
@user = User.find(params[:user_id])
|
|
key = "reported_#{@user.id}"
|
|
if !session[key]
|
|
session[key] = true
|
|
@user.report_avatar_image!
|
|
end
|
|
render :json => {:ok => true}
|
|
end
|
|
|
|
def update_avatar_image
|
|
@user = User.find(params[:user_id])
|
|
if authorized_action(@user, @current_user, :remove_avatar)
|
|
@user.avatar_state = params[:avatar][:state]
|
|
@user.save
|
|
render :json => @user.as_json(:include_root => false)
|
|
end
|
|
end
|
|
|
|
def public_feed
|
|
return unless get_feed_context(:only => [:user])
|
|
feed = Atom::Feed.new do |f|
|
|
f.title = "#{@context.name} Feed"
|
|
f.links << Atom::Link.new(:href => dashboard_url, :rel => 'self')
|
|
f.updated = Time.now
|
|
f.id = user_url(@context)
|
|
end
|
|
@entries = []
|
|
cutoff = 1.week.ago
|
|
@context.courses.each do |context|
|
|
@entries.concat context.assignments.active.where("updated_at>?", cutoff)
|
|
@entries.concat context.calendar_events.active.where("updated_at>?", cutoff)
|
|
@entries.concat context.discussion_topics.active.where("updated_at>?", cutoff)
|
|
@entries.concat context.wiki.wiki_pages.not_deleted.where("updated_at>?", cutoff)
|
|
end
|
|
@entries.each do |entry|
|
|
feed.entries << entry.to_atom(:include_context => true, :context => @context)
|
|
end
|
|
respond_to do |format|
|
|
format.atom { render :plain => feed.to_xml }
|
|
end
|
|
end
|
|
|
|
def teacher_activity
|
|
@teacher = User.find(params[:user_id])
|
|
if @teacher == @current_user || authorized_action(@teacher, @current_user, :read_reports)
|
|
@courses = {}
|
|
|
|
if params[:student_id]
|
|
student = User.find(params[:student_id])
|
|
enrollments = student.student_enrollments.active.preload(:course).shard(student).to_a
|
|
enrollments.each do |enrollment|
|
|
should_include = enrollment.course.user_has_been_instructor?(@teacher) &&
|
|
enrollment.course.grants_right?(@current_user, :read_reports) &&
|
|
enrollment.course.apply_enrollment_visibility(enrollment.course.all_student_enrollments, @teacher).where(id: enrollment).first
|
|
if should_include
|
|
Enrollment.recompute_final_score_if_stale(enrollment.course, student) { enrollment.reload }
|
|
@courses[enrollment.course] = teacher_activity_report(@teacher, enrollment.course, [enrollment])
|
|
end
|
|
end
|
|
|
|
if @courses.all? { |c, e| e.blank? }
|
|
flash[:error] = t('errors.no_teacher_courses', "There are no courses shared between this teacher and student")
|
|
redirect_to_referrer_or_default(root_url)
|
|
end
|
|
|
|
else # implied params[:course_id]
|
|
course = Course.find(params[:course_id])
|
|
if !course.user_has_been_instructor?(@teacher)
|
|
flash[:error] = t('errors.user_not_teacher', "That user is not a teacher in this course")
|
|
redirect_to_referrer_or_default(root_url)
|
|
elsif authorized_action(course, @current_user, :read_reports)
|
|
Enrollment.recompute_final_score_if_stale(course)
|
|
enrollments = course.apply_enrollment_visibility(course.all_student_enrollments, @teacher)
|
|
@courses[course] = teacher_activity_report(@teacher, course, enrollments)
|
|
end
|
|
end
|
|
|
|
end
|
|
end
|
|
|
|
def avatar_image
|
|
cancel_cache_buster
|
|
# TODO: remove support for specifying user ids by id, require using
|
|
# the encrypted version. We can't do it right away because there are
|
|
# a bunch of places that will have cached fragments using the old
|
|
# style.
|
|
return redirect_to(User.default_avatar_fallback) unless service_enabled?(:avatars)
|
|
user_id = params[:user_id].to_i
|
|
if params[:user_id].present? && params[:user_id].match(/-/)
|
|
user_id = User.user_id_from_avatar_key(params[:user_id])
|
|
end
|
|
account_avatar_setting = service_enabled?(:avatars) ? @domain_root_account.settings[:avatars] || 'enabled' : 'disabled'
|
|
user_id, user_shard = Shard.local_id_for(user_id)
|
|
user_shard ||= Shard.current
|
|
url = user_shard.activate do
|
|
Rails.cache.fetch(Cacher.avatar_cache_key(user_id, account_avatar_setting)) do
|
|
user = User.where(id: user_id).first if user_id.present?
|
|
if user
|
|
user.avatar_url(nil, account_avatar_setting, "%{fallback}")
|
|
else
|
|
'%{fallback}'
|
|
end
|
|
end
|
|
end
|
|
fallback = User.avatar_fallback_url(nil, request)
|
|
redirect_to (url.blank? || url == "%{fallback}") ?
|
|
User.default_avatar_fallback :
|
|
url.sub(CGI.escape("%{fallback}"), CGI.escape(fallback))
|
|
end
|
|
|
|
# @API Merge user into another user
|
|
#
|
|
# Merge a user into another user.
|
|
# To merge users, the caller must have permissions to manage both users. This
|
|
# should be considered irreversible. This will delete the user and move all
|
|
# the data into the destination user.
|
|
#
|
|
# When finding users by SIS ids in different accounts the
|
|
# destination_account_id is required.
|
|
#
|
|
# The account can also be identified by passing the domain in destination_account_id.
|
|
#
|
|
# @example_request
|
|
# curl https://<canvas>/api/v1/users/<user_id>/merge_into/<destination_user_id> \
|
|
# -X PUT \
|
|
# -H 'Authorization: Bearer <token>'
|
|
#
|
|
# @example_request
|
|
# curl https://<canvas>/api/v1/users/<user_id>/merge_into/accounts/<destination_account_id>/users/<destination_user_id> \
|
|
# -X PUT \
|
|
# -H 'Authorization: Bearer <token>'
|
|
#
|
|
# @returns User
|
|
def merge_into
|
|
user = api_find(User, params[:id])
|
|
if authorized_action(user, @current_user, :merge)
|
|
|
|
if (account_id = params[:destination_account_id])
|
|
destination_account = Account.find_by_domain(account_id)
|
|
destination_account ||= Account.find(account_id)
|
|
else
|
|
destination_account ||= @domain_root_account
|
|
end
|
|
|
|
into_user = api_find(User, params[:destination_user_id], account: destination_account)
|
|
|
|
if authorized_action(into_user, @current_user, :merge)
|
|
UserMerge.from(user).into into_user
|
|
render(:json => user_json(into_user,
|
|
@current_user,
|
|
session,
|
|
%w{locale},
|
|
destination_account))
|
|
end
|
|
end
|
|
end
|
|
|
|
# @API Split merged users into separate users
|
|
#
|
|
# Merged users cannot be fully restored to their previous state, but this will
|
|
# attempt to split as much as possible to the previous state.
|
|
# To split a merged user, the caller must have permissions to manage all of
|
|
# the users logins. If there are multiple users that have been merged into one
|
|
# user it will split each merge into a separate user.
|
|
# A split can only happen within 180 days of a user merge. A user merge deletes
|
|
# the previous user and may be permanently deleted. In this scenario we create
|
|
# a new user object and proceed to move as much as possible to the new user.
|
|
# The user object will not have preserved the name or settings from the
|
|
# previous user. Some items may have been deleted during a user_merge that
|
|
# cannot be restored, and/or the data has become stale because of other
|
|
# changes to the objects since the time of the user_merge.
|
|
#
|
|
# @example_request
|
|
# curl https://<canvas>/api/v1/users/<user_id>/split \
|
|
# -X POST \
|
|
# -H 'Authorization: Bearer <token>'
|
|
#
|
|
# @returns [User]
|
|
def split
|
|
user = api_find(User, params[:id])
|
|
unless UserMergeData.active.splitable.where(user_id: user).shard(user).exists?
|
|
return render json: {message: t('Nothing to split off of this user')}, status: :bad_request
|
|
end
|
|
|
|
if authorized_action(user, @current_user, :merge)
|
|
users = SplitUsers.split_db_users(user)
|
|
render :json => users.map { |u| user_json(u, @current_user, session) }
|
|
end
|
|
end
|
|
|
|
# maybe I should document this
|
|
# maybe not
|
|
# basically does the same thing as UserList#users
|
|
def invite_users
|
|
# pass into "users" an array of hashes with "email"
|
|
# e.g. [{"email": "email@example.com"}]
|
|
# also can include an optional "name"
|
|
|
|
# returns the original list in :invited_users (with ids) if successfully added, or in :errored_users if not
|
|
get_context
|
|
return unless authorized_action(@context, @current_user, [:manage_students, :manage_admin_users])
|
|
|
|
root_account = context.root_account
|
|
unless root_account.open_registration? || root_account.grants_right?(@current_user, session, :manage_user_logins)
|
|
return render_unauthorized_action
|
|
end
|
|
|
|
invited_users = []
|
|
errored_users = []
|
|
Array(params[:users]).each do |user_hash|
|
|
unless user_hash[:email].present?
|
|
errored_users << user_hash.merge(:error => "email required")
|
|
next
|
|
end
|
|
|
|
email = user_hash[:email]
|
|
user = User.new(:name => user_hash[:name] || email)
|
|
cc = user.communication_channels.build(:path => email, :path_type => 'email')
|
|
cc.user = user
|
|
user.workflow_state = 'creation_pending'
|
|
|
|
# check just in case
|
|
existing_rows = Pseudonym.active.where(:account_id => @context.root_account).joins(:user => :communication_channels).joins(:account).
|
|
where("communication_channels.workflow_state<>'retired' AND path_type='email' AND LOWER(path) = ?", email.downcase).
|
|
pluck('communication_channels.path', :user_id, :account_id, 'users.name', 'accounts.name')
|
|
|
|
if existing_rows.any?
|
|
existing_users = existing_rows.map do |address, user_id, account_id, user_name, account_name|
|
|
{:address => address, :user_id => user_id, :user_name => user_name, :account_id => account_id, :account_name => account_name}
|
|
end
|
|
errored_users << user_hash.merge(:errors => [{:message => "Matching user(s) already exist"}], :existing_users => existing_users)
|
|
elsif user.save
|
|
invited_users << user_hash.merge(:id => user.id)
|
|
else
|
|
errored_users << user_hash.merge(user.errors.as_json)
|
|
end
|
|
end
|
|
render :json => {:invited_users => invited_users, :errored_users => errored_users}
|
|
end
|
|
|
|
protected
|
|
|
|
def teacher_activity_report(teacher, course, student_enrollments)
|
|
ids = student_enrollments.map(&:user_id)
|
|
data = {}
|
|
student_enrollments.each { |e| data[e.user.id] = { :enrollment => e, :ungraded => [] } }
|
|
|
|
# find last interactions
|
|
last_comment_dates = SubmissionCommentInteraction.in_course_between(course, teacher.id, ids)
|
|
last_comment_dates.each do |(user_id, author_id), date|
|
|
next unless student = data[user_id]
|
|
student[:last_interaction] = [student[:last_interaction], date].compact.max
|
|
end
|
|
scope = ConversationMessage.
|
|
joins("INNER JOIN #{ConversationParticipant.quoted_table_name} ON conversation_participants.conversation_id=conversation_messages.conversation_id").
|
|
where('conversation_messages.author_id = ? AND conversation_participants.user_id IN (?) AND NOT conversation_messages.generated', teacher, ids)
|
|
# fake_arel can't pass an array in the group by through the scope
|
|
last_message_dates = scope.group(['conversation_participants.user_id', 'conversation_messages.author_id']).maximum(:created_at)
|
|
last_message_dates.each do |key, date|
|
|
next unless student = data[key.first.to_i]
|
|
student[:last_interaction] = [student[:last_interaction], date].compact.max
|
|
end
|
|
|
|
# find all ungraded submissions in one query
|
|
ungraded_submissions = course.submissions.
|
|
preload(:assignment).
|
|
where("user_id IN (?) AND #{Submission.needs_grading_conditions}", ids).
|
|
except(:order).
|
|
order(:submitted_at).to_a
|
|
|
|
|
|
ungraded_submissions.each do |submission|
|
|
next unless student = data[submission.user_id]
|
|
student[:ungraded] << submission
|
|
end
|
|
|
|
if course.root_account.enable_user_notes?
|
|
data.each { |k,v| v[:last_user_note] = nil }
|
|
# find all last user note times in one query
|
|
note_dates = UserNote.active.
|
|
group(:user_id).
|
|
where("created_by_id = ? AND user_id IN (?)", teacher, ids).
|
|
maximum(:created_at)
|
|
note_dates.each do |user_id, date|
|
|
next unless student = data[user_id]
|
|
student[:last_user_note] = date
|
|
end
|
|
end
|
|
|
|
|
|
Canvas::ICU.collate_by(data.values) { |e| e[:enrollment].user.sortable_name }
|
|
end
|
|
|
|
protected
|
|
|
|
def require_self_registration
|
|
get_context
|
|
@context = @domain_root_account || Account.default unless @context.is_a?(Account)
|
|
@context = @context.root_account
|
|
unless @context.grants_right?(@current_user, session, :manage_user_logins) ||
|
|
@context.self_registration_allowed_for?(params[:user] && params[:user][:initial_enrollment_type])
|
|
flash[:error] = t('no_self_registration', "Self registration has not been enabled for this account")
|
|
respond_to do |format|
|
|
format.html { redirect_to root_url }
|
|
format.json { render :json => {}, :status => 403 }
|
|
end
|
|
return false
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def generate_grading_period_id(period_id)
|
|
# nil and '' will get converted to 0 in the .to_i call
|
|
id = period_id.to_i
|
|
id == 0 ? nil : id
|
|
end
|
|
|
|
def render_new_user_tutorial_statuses(user)
|
|
render(json: { new_user_tutorial_statuses: { collapsed: user.new_user_tutorial_statuses }})
|
|
end
|
|
|
|
def authenticate_observee
|
|
Pseudonym.authenticate(params[:observee] || {},
|
|
[@domain_root_account.id] + @domain_root_account.trusted_account_ids)
|
|
end
|
|
|
|
def grades_for_presenter(presenter, grading_periods)
|
|
grades = {
|
|
student_enrollments: {},
|
|
observed_enrollments: {}
|
|
}
|
|
grouped_observed_enrollments =
|
|
presenter.observed_enrollments.group_by { |enrollment| enrollment[:course_id] }
|
|
|
|
grouped_observed_enrollments.each do |course_id, enrollments|
|
|
grading_period_id = generate_grading_period_id(
|
|
grading_periods.dig(course_id, :selected_period_id)
|
|
)
|
|
grades[:observed_enrollments][course_id] = {}
|
|
grades[:observed_enrollments][course_id] = grades_from_enrollments(
|
|
enrollments,
|
|
grading_period_id: grading_period_id
|
|
)
|
|
end
|
|
|
|
presenter.student_enrollments.each do |course, enrollment|
|
|
grading_period_id = generate_grading_period_id(
|
|
grading_periods.dig(course.id, :selected_period_id)
|
|
)
|
|
computed_score = enrollment.computed_current_score(grading_period_id: grading_period_id)
|
|
grades[:student_enrollments][course.id] = computed_score
|
|
end
|
|
grades
|
|
end
|
|
|
|
def grades_from_enrollments(enrollments, grading_period_id: nil)
|
|
grades = {}
|
|
enrollments.each do |enrollment|
|
|
computed_score = enrollment.computed_current_score(grading_period_id: grading_period_id)
|
|
grades[enrollment.user_id] = computed_score
|
|
end
|
|
grades
|
|
end
|
|
|
|
def collected_grading_periods_for_presenter(presenter, course_id, grading_period_id)
|
|
observer_courses = presenter.observed_enrollments.map(&:course)
|
|
student_courses = presenter.student_enrollments.map(&:first)
|
|
teacher_courses = presenter.teacher_enrollments.map(&:course)
|
|
courses = observer_courses | student_courses | teacher_courses
|
|
grading_periods = {}
|
|
|
|
courses.each do |course|
|
|
next unless course.grading_periods?
|
|
|
|
course_periods = GradingPeriod.for(course)
|
|
grading_period_specified = grading_period_id &&
|
|
course_id && course_id.to_i == course.id
|
|
|
|
selected_period_id = if grading_period_specified
|
|
grading_period_id.to_i
|
|
else
|
|
current_period = course_periods.find(&:current?)
|
|
current_period ? current_period.id : 0
|
|
end
|
|
|
|
grading_periods[course.id] = {
|
|
periods: course_periods,
|
|
selected_period_id: selected_period_id
|
|
}
|
|
end
|
|
grading_periods
|
|
end
|
|
|
|
def create_user
|
|
run_login_hooks
|
|
# Look for an incomplete registration with this pseudonym
|
|
|
|
sis_user_id = nil
|
|
integration_id = nil
|
|
params[:pseudonym] ||= {}
|
|
|
|
if @context.grants_right?(@current_user, session, :manage_sis)
|
|
sis_user_id = params[:pseudonym].delete(:sis_user_id)
|
|
integration_id = params[:pseudonym].delete(:integration_id)
|
|
end
|
|
|
|
@pseudonym = nil
|
|
@user = nil
|
|
if sis_user_id && value_to_boolean(params[:enable_sis_reactivation])
|
|
@pseudonym = @context.pseudonyms.where(:sis_user_id => sis_user_id, :workflow_state => 'deleted').first
|
|
if @pseudonym
|
|
@pseudonym.workflow_state = 'active'
|
|
@pseudonym.save!
|
|
@user = @pseudonym.user
|
|
@user.workflow_state = 'registered'
|
|
@user.update_account_associations
|
|
end
|
|
end
|
|
|
|
if @pseudonym.nil?
|
|
@pseudonym = @context.pseudonyms.active.by_unique_id(params[:pseudonym][:unique_id]).first
|
|
# Setting it to nil will cause us to try and create a new one, and give user the login already exists error
|
|
@pseudonym = nil if @pseudonym && !['creation_pending', 'pending_approval'].include?(@pseudonym.user.workflow_state)
|
|
end
|
|
|
|
@user ||= @pseudonym && @pseudonym.user
|
|
@user ||= User.new
|
|
|
|
force_validations = value_to_boolean(params[:force_validations])
|
|
manage_user_logins = @context.grants_right?(@current_user, session, :manage_user_logins)
|
|
self_enrollment = params[:self_enrollment].present?
|
|
allow_non_email_pseudonyms = !force_validations && manage_user_logins || self_enrollment && params[:pseudonym_type] == 'username'
|
|
require_password = self_enrollment && allow_non_email_pseudonyms
|
|
allow_password = require_password || manage_user_logins
|
|
|
|
notify_policy = Users::CreationNotifyPolicy.new(manage_user_logins, params[:pseudonym])
|
|
|
|
includes = %w{locale}
|
|
|
|
cc_params = params[:communication_channel]
|
|
|
|
if cc_params
|
|
cc_type = cc_params[:type] || CommunicationChannel::TYPE_EMAIL
|
|
cc_addr = cc_params[:address] || params[:pseudonym][:unique_id]
|
|
|
|
if cc_type == CommunicationChannel::TYPE_EMAIL
|
|
cc_addr = nil unless EmailAddressValidator.valid?(cc_addr)
|
|
end
|
|
|
|
can_manage_students = [Account.site_admin, @context].any? do |role|
|
|
role.grants_right?(@current_user, :manage_students)
|
|
end
|
|
|
|
if can_manage_students
|
|
skip_confirmation = value_to_boolean(cc_params[:skip_confirmation])
|
|
end
|
|
|
|
if can_manage_students && cc_type == CommunicationChannel::TYPE_EMAIL
|
|
includes << 'confirmation_url' if value_to_boolean(cc_params[:confirmation_url])
|
|
end
|
|
|
|
else
|
|
cc_type = CommunicationChannel::TYPE_EMAIL
|
|
cc_addr = params[:pseudonym].delete(:path) || params[:pseudonym][:unique_id]
|
|
cc_addr = nil unless EmailAddressValidator.valid?(cc_addr)
|
|
end
|
|
|
|
if params[:user]
|
|
user_params = params[:user].
|
|
permit(:name, :short_name, :sortable_name, :time_zone, :show_user_services, :gender,
|
|
:avatar_image, :subscribe_to_emails, :locale, :bio, :birthdate, :terms_of_use,
|
|
:self_enrollment_code, :initial_enrollment_type)
|
|
if self_enrollment && user_params[:self_enrollment_code]
|
|
user_params[:self_enrollment_code].strip!
|
|
else
|
|
user_params.delete(:self_enrollment_code)
|
|
end
|
|
if user_params[:birthdate].present? && user_params[:birthdate] !~ Api::ISO8601_REGEX &&
|
|
user_params[:birthdate] !~ Api::DATE_REGEX
|
|
return render(:json => {:errors => {:birthdate => t(:birthdate_invalid,
|
|
'Invalid date or invalid datetime for birthdate')}}, :status => 400)
|
|
end
|
|
|
|
@user.attributes = user_params
|
|
accepted_terms = params[:user].delete(:terms_of_use)
|
|
@user.accept_terms if value_to_boolean(accepted_terms)
|
|
includes << "terms_of_use" unless accepted_terms.nil?
|
|
end
|
|
@user.name ||= params[:pseudonym][:unique_id]
|
|
skip_registration = value_to_boolean(params[:user].try(:[], :skip_registration))
|
|
unless @user.registered?
|
|
@user.workflow_state = if require_password || skip_registration
|
|
# no email confirmation required (self_enrollment_code and password
|
|
# validations will ensure everything is legit)
|
|
'registered'
|
|
elsif notify_policy.is_self_registration? && @user.registration_approval_required?
|
|
'pending_approval'
|
|
else
|
|
'pre_registered'
|
|
end
|
|
end
|
|
if force_validations || !manage_user_logins
|
|
@user.require_acceptance_of_terms = @domain_root_account.terms_required?
|
|
@user.require_presence_of_name = true
|
|
@user.require_self_enrollment_code = self_enrollment
|
|
@user.validation_root_account = @domain_root_account
|
|
end
|
|
|
|
@invalid_observee_creds = nil
|
|
if @user.initial_enrollment_type == 'observer'
|
|
if (observee_pseudonym = authenticate_observee)
|
|
@observee = observee_pseudonym.user
|
|
else
|
|
@invalid_observee_creds = Pseudonym.new
|
|
@invalid_observee_creds.errors.add('unique_id', 'bad_credentials')
|
|
end
|
|
end
|
|
|
|
@pseudonym ||= @user.pseudonyms.build(:account => @context)
|
|
@pseudonym.account.email_pseudonyms = !allow_non_email_pseudonyms
|
|
@pseudonym.require_password = require_password
|
|
# pre-populate the reverse association
|
|
@pseudonym.user = @user
|
|
|
|
pseudonym_params = params[:pseudonym] ?
|
|
params[:pseudonym].permit(:password, :password_confirmation, :unique_id) : {}
|
|
# don't require password_confirmation on api calls
|
|
pseudonym_params[:password_confirmation] = pseudonym_params[:password] if api_request?
|
|
# don't allow password setting for new users that are not self-enrolling
|
|
# in a course (they need to go the email route)
|
|
unless allow_password
|
|
pseudonym_params.delete(:password)
|
|
pseudonym_params.delete(:password_confirmation)
|
|
end
|
|
if params[:pseudonym][:authentication_provider_id]
|
|
@pseudonym.authentication_provider = @context.
|
|
authentication_providers.active.
|
|
find(params[:pseudonym][:authentication_provider_id])
|
|
end
|
|
@pseudonym.attributes = pseudonym_params
|
|
@pseudonym.sis_user_id = sis_user_id
|
|
@pseudonym.integration_id = integration_id
|
|
|
|
@pseudonym.account = @context
|
|
@pseudonym.workflow_state = 'active'
|
|
if cc_addr.present?
|
|
@cc =
|
|
@user.communication_channels.where(:path_type => cc_type).by_path(cc_addr).first ||
|
|
@user.communication_channels.build(:path_type => cc_type, :path => cc_addr)
|
|
@cc.user = @user
|
|
@cc.workflow_state = skip_confirmation ? 'active' : 'unconfirmed' unless @cc.workflow_state == 'confirmed'
|
|
end
|
|
|
|
if @user.valid? && @pseudonym.valid? && @invalid_observee_creds.nil?
|
|
# saving the user takes care of the @pseudonym and @cc, so we can't call
|
|
# save_without_session_maintenance directly. we don't want to auto-log-in
|
|
# unless the user is registered/pre_registered (if the latter, he still
|
|
# needs to confirm his email and set a password, otherwise he can't get
|
|
# back in once his session expires)
|
|
if !@current_user # automagically logged in
|
|
PseudonymSession.new(@pseudonym).save unless @pseudonym.new_record?
|
|
else
|
|
@pseudonym.send(:skip_session_maintenance=, true)
|
|
end
|
|
@user.save!
|
|
if @observee && !@user.user_observees.where(user_id: @observee).exists?
|
|
@user.user_observees << @user.user_observees.create_or_restore(user_id: @observee)
|
|
end
|
|
|
|
if notify_policy.is_self_registration?
|
|
registration_params = params.fetch(:user, {}).merge(remote_ip: request.remote_ip, cookies: cookies)
|
|
@user.new_registration(registration_params)
|
|
end
|
|
message_sent = notify_policy.dispatch!(@user, @pseudonym, @cc) if @cc
|
|
|
|
data = { :user => @user, :pseudonym => @pseudonym, :channel => @cc, :message_sent => message_sent, :course => @user.self_enrollment_course }
|
|
if api_request?
|
|
render(:json => user_json(@user, @current_user, session, includes))
|
|
else
|
|
render(:json => data)
|
|
end
|
|
else
|
|
errors = {
|
|
:errors => {
|
|
:user => @user.errors.as_json[:errors],
|
|
:pseudonym => @pseudonym ? @pseudonym.errors.as_json[:errors] : {},
|
|
:observee => @invalid_observee_creds ? @invalid_observee_creds.errors.as_json[:errors] : {}
|
|
}
|
|
}
|
|
render :json => errors, :status => :bad_request
|
|
end
|
|
end
|
|
end
|