1558 lines
59 KiB
Ruby
1558 lines
59 KiB
Ruby
#
|
|
# Copyright (C) 2011 - 2013 Instructure, Inc.
|
|
#
|
|
# This file is part of Canvas.
|
|
#
|
|
# Canvas is free software: you can redistribute it and/or modify it under
|
|
# the terms of the GNU Affero General Public License as published by the Free
|
|
# Software Foundation, version 3 of the License.
|
|
#
|
|
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
# details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License along
|
|
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
|
|
# @API 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 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.",
|
|
# "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": "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"
|
|
# },
|
|
# "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.",
|
|
# "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 ceratin API calls, and will return the IANA time zone name of the user's preferred timezone.",
|
|
# "example": "America/Denver",
|
|
# "type": "string"
|
|
# }
|
|
# }
|
|
# }
|
|
class UsersController < ApplicationController
|
|
|
|
|
|
include GoogleDocs
|
|
include Twitter
|
|
include LinkedIn
|
|
include DeliciousDiigo
|
|
include SearchHelper
|
|
include I18nUtilities
|
|
|
|
before_filter :require_user, :only => [:grades, :merge, :kaltura_session,
|
|
:ignore_item, :ignore_stream_item, :close_notification, :mark_avatar_image,
|
|
:user_dashboard, :toggle_dashboard, :masquerade, :external_tool,
|
|
:dashboard_sidebar, :settings, :all_menu_courses]
|
|
before_filter :require_registered_user, :only => [:delete_user_service,
|
|
:create_user_service]
|
|
before_filter :reject_student_view_student, :only => [:delete_user_service,
|
|
:create_user_service, :merge, :user_dashboard, :masquerade]
|
|
before_filter :require_self_registration, :only => [:new, :create]
|
|
|
|
def grades
|
|
@user = User.find_by_id(params[:user_id]) if params[:user_id].present?
|
|
@user ||= @current_user
|
|
if authorized_action(@user, @current_user, :read)
|
|
current_active_enrollments = @user.current_enrollments.with_each_shard { |scope| scope.includes(:course) }
|
|
|
|
@presenter = GradesPresenter.new(current_active_enrollments)
|
|
|
|
if @presenter.has_single_enrollment?
|
|
redirect_to course_grades_url(@presenter.single_enrollment.course_id)
|
|
return
|
|
end
|
|
|
|
Enrollment.send(:preload_associations, @observed_enrollments, :course)
|
|
end
|
|
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_docs"
|
|
redirect_to google_docs_request_token_url(return_to_url)
|
|
elsif params[:service] == "twitter"
|
|
redirect_to twitter_request_token_url(return_to_url)
|
|
elsif params[:service] == "linked_in"
|
|
redirect_to linked_in_request_token_url(return_to_url)
|
|
elsif params[:service] == "facebook"
|
|
oauth_request = OauthRequest.create(
|
|
:service => 'facebook',
|
|
:secret => AutoHandle.generate("fb", 10),
|
|
:return_url => return_to_url,
|
|
:user => @current_user,
|
|
:original_host_with_port => request.host_with_port
|
|
)
|
|
redirect_to Facebook.authorize_url(oauth_request)
|
|
end
|
|
end
|
|
|
|
def oauth_success
|
|
oauth_request = nil
|
|
if params[:oauth_token]
|
|
oauth_request = OauthRequest.find_by_token_and_service(params[:oauth_token], params[:service])
|
|
elsif params[:state] && params[:service] == 'facebook'
|
|
oauth_request = OauthRequest.find_by_id(Facebook.oauth_request_id(params[:state]))
|
|
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] == "facebook"
|
|
service = Facebook.authorize_success(@current_user, params[:access_token])
|
|
if service
|
|
flash[:notice] = t('facebook_added', "Facebook account successfully added!")
|
|
else
|
|
flash[:error] = t('facebook_fail', "Facebook authorization failed.")
|
|
end
|
|
elsif params[:service] == "google_docs"
|
|
begin
|
|
google_docs_get_access_token(oauth_request, params[:oauth_verifier])
|
|
flash[:notice] = t('google_docs_added', "Google Docs access authorized!")
|
|
rescue => e
|
|
ErrorReport.log_exception(:oauth, e)
|
|
flash[:error] = t('google_docs_fail', "Google Docs authorization failed. Please try again")
|
|
end
|
|
elsif params[:service] == "linked_in"
|
|
begin
|
|
linked_in_get_access_token(oauth_request, params[:oauth_verifier])
|
|
flash[:notice] = t('linkedin_added', "LinkedIn account successfully added!")
|
|
rescue => e
|
|
ErrorReport.log_exception(:oauth, e)
|
|
flash[:error] = t('linkedin_fail', "LinkedIn authorization failed. Please try again")
|
|
end
|
|
else
|
|
begin
|
|
token = twitter_get_access_token(oauth_request, params[:oauth_verifier])
|
|
flash[:notice] = t('twitter_added', "Twitter access authorized!")
|
|
rescue => e
|
|
ErrorReport.log_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 [Optional, String]
|
|
# The partial name or full ID of the users to match and return in the
|
|
# results list. Must be at least 3 characters.
|
|
#
|
|
# @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(User.enrollment_conditions(:active)).
|
|
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
|
|
|
|
if search_term
|
|
users = UserSearch.for_user_in_context(search_term, @context, @current_user, session)
|
|
else
|
|
users = UserSearch.scope_for(@context, @current_user)
|
|
end
|
|
|
|
users = Api.paginate(users, self, api_v1_account_users_url)
|
|
user_json_preloads(users)
|
|
return render :json => users.map { |u| user_json(u, @current_user, session) }
|
|
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.order(EnrollmentTerm.nulls(:first, :start_at))
|
|
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_filter :require_password_session, :only => [:masquerade]
|
|
def masquerade
|
|
@user = User.find_by_id(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)
|
|
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
|
|
|
|
def user_dashboard
|
|
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
|
|
|
|
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
|
|
|
|
@announcements = AccountNotification.for_user_and_account(@current_user, @domain_root_account)
|
|
@pending_invitations = @current_user.cached_current_enrollments(:include_enrollment_uuid => session[:enrollment_uuid]).select { |e| e.invited? }
|
|
@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 :contexts => ([user] + user.cached_contexts)
|
|
end
|
|
end
|
|
|
|
def cached_submissions(user, upcoming_events)
|
|
Rails.cache.fetch(['cached_user_submissions', 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, :workflow_state]).
|
|
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.present?)
|
|
@recent_feedback = (@current_user && @current_user.recent_feedback) || []
|
|
end
|
|
end
|
|
|
|
render :layout => false
|
|
end
|
|
|
|
def toggle_dashboard
|
|
@current_user.preferences[:new_dashboard] = !@current_user.preferences[:new_dashboard]
|
|
@current_user.save!
|
|
render :json => {}
|
|
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|...',
|
|
# '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
|
|
# }
|
|
def activity_stream
|
|
if @current_user
|
|
api_render_stream_for_contexts(nil, :api_v1_user_activity_stream_url)
|
|
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)
|
|
|
|
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.
|
|
#
|
|
# 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,
|
|
# }
|
|
# ]
|
|
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().map { |a| todo_item_json(a, @current_user, session, 'submitting') }
|
|
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
|
|
|
|
def ignore_item
|
|
unless %w[grading submitting].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']),
|
|
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_docs"
|
|
Rails.cache.delete(['google_docs_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_get_bookmarks(service, 1)
|
|
when 'skype'
|
|
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.find_by_type_and_service('BookmarkService', params[:service_type]) 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, :view_statistics)
|
|
add_crumb(t('crumbs.profile', "%{user}'s profile", :user => @user.short_name), @user == @current_user ? user_profile_path(@current_user) : user_path(@user) )
|
|
|
|
# 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.with_each_shard { |scope| scope.where("enrollments.workflow_state<>'deleted' AND courses.workflow_state<>'deleted'").includes({:course => { :enrollment_term => :enrollment_dates_overrides }}, :associated_user, :course_section) }
|
|
@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 }
|
|
@group_memberships = @user.current_group_memberships.includes(:group)
|
|
|
|
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
|
|
|
|
def external_tool
|
|
@tool = ContextExternalTool.find_for(params[:id], @domain_root_account, :user_navigation)
|
|
@resource_title = @tool.label_for(:user_navigation)
|
|
@resource_url = @tool.user_navigation(:url)
|
|
@opaque_id = @tool.opaque_identifier_for(@current_user)
|
|
@resource_type = 'user_navigation'
|
|
@return_url = user_profile_url(@current_user, :include_host => true)
|
|
@launch = BasicLTI::ToolLaunch.new(:url => @resource_url, :tool => @tool, :user => @current_user, :context => @domain_root_account, :link_code => @opaque_id, :return_url => @return_url, :resource_type => @resource_type)
|
|
@tool_settings = @launch.generate
|
|
@active_tab = @tool.asset_string
|
|
add_crumb(@current_user.short_name, user_profile_path(@current_user))
|
|
render :template => 'external_tools/tool_show'
|
|
end
|
|
|
|
def new
|
|
return redirect_to(root_url) if @current_user
|
|
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] [Optional, 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] [Optional, String]
|
|
# User's name as it will be displayed in discussions, messages, and comments.
|
|
#
|
|
# @argument user[sortable_name] [Optional, String]
|
|
# User's name as used to sort alphabetically in lists.
|
|
#
|
|
# @argument user[time_zone] [Optional, 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] [Optional, String]
|
|
# The user's preferred language as a two-letter ISO 639-1 code.
|
|
#
|
|
# @argument user[birthdate] [Optional, Date]
|
|
# The user's birth date.
|
|
#
|
|
# @argument user[terms_of_use] [Optional, 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).
|
|
#
|
|
# @argument pseudonym[unique_id] [String]
|
|
# User's login ID. If this is a self-registration, it must be a valid
|
|
# email address.
|
|
#
|
|
# @argument pseudonym[password] [Optional, String]
|
|
# User's password. Cannot be set during self-registration.
|
|
#
|
|
# @argument pseudonym[sis_user_id] [Optional, String]
|
|
# SIS ID for the user's account. To set this parameter, the caller must be
|
|
# able to manage SIS permissions.
|
|
#
|
|
# @argument pseudonym[send_confirmation] [Optional, Boolean]
|
|
# Send user notification of account creation if true.
|
|
# Automatically set to true during self-registration.
|
|
#
|
|
# @argument communication_channel[type] [Optional, String]
|
|
# The communication channel type, e.g. 'email' or 'sms'.
|
|
#
|
|
# @argument communication_channel[address] [Optional, String]
|
|
# The communication channel address, e.g. the user's email address.
|
|
#
|
|
# @returns User
|
|
def create
|
|
# Look for an incomplete registration with this pseudonym
|
|
@pseudonym = @context.pseudonyms.active.custom_find_by_unique_id(params[:pseudonym][:unique_id])
|
|
# 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)
|
|
|
|
manage_user_logins = @context.grants_right?(@current_user, session, :manage_user_logins)
|
|
self_enrollment = params[:self_enrollment].present?
|
|
allow_non_email_pseudonyms = 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 = value_to_boolean(params[:pseudonym].delete(:send_confirmation))
|
|
notify = :self_registration unless manage_user_logins
|
|
|
|
if params[:communication_channel]
|
|
cc_type = params[:communication_channel][:type] || CommunicationChannel::TYPE_EMAIL
|
|
cc_addr = params[:communication_channel][:address]
|
|
else
|
|
cc_type = CommunicationChannel::TYPE_EMAIL
|
|
cc_addr = params[:pseudonym].delete(:path) || params[:pseudonym][:unique_id]
|
|
end
|
|
|
|
sis_user_id = params[:pseudonym].delete(:sis_user_id)
|
|
sis_user_id = nil unless @context.grants_right?(@current_user, session, :manage_sis)
|
|
|
|
@user = @pseudonym && @pseudonym.user
|
|
@user ||= User.new
|
|
if params[:user]
|
|
params[:user].delete(:self_enrollment_code) unless self_enrollment
|
|
@user.attributes = params[:user]
|
|
end
|
|
@user.name ||= params[:pseudonym][:unique_id]
|
|
unless @user.registered?
|
|
@user.workflow_state = if require_password
|
|
# no email confirmation required (self_enrollment_code and password
|
|
# validations will ensure everything is legit)
|
|
'registered'
|
|
elsif notify == :self_registration && @user.registration_approval_required?
|
|
'pending_approval'
|
|
else
|
|
'pre_registered'
|
|
end
|
|
end
|
|
if !manage_user_logins # i.e. a new user signing up
|
|
@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
|
|
|
|
@observee = nil
|
|
if @user.initial_enrollment_type == 'observer'
|
|
# TODO: SAML/CAS support
|
|
if observee = Pseudonym.authenticate(params[:observee] || {},
|
|
[@domain_root_account.id] + @domain_root_account.trusted_account_ids)
|
|
@user.observed_users << observee.user unless @user.observed_users.include?(observee.user)
|
|
else
|
|
@observee = Pseudonym.new
|
|
@observee.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
|
|
# don't require password_confirmation on api calls
|
|
params[:pseudonym][:password_confirmation] = params[:pseudonym][: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
|
|
params[:pseudonym].delete(:password)
|
|
params[:pseudonym].delete(:password_confirmation)
|
|
end
|
|
@pseudonym.attributes = params[:pseudonym]
|
|
@pseudonym.sis_user_id = sis_user_id
|
|
|
|
@pseudonym.account = @context
|
|
@pseudonym.workflow_state = 'active'
|
|
@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 = 'unconfirmed' unless @cc.workflow_state == 'confirmed'
|
|
|
|
if @user.valid? && @pseudonym.valid? && @observee.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!
|
|
message_sent = false
|
|
if notify == :self_registration
|
|
unless @user.pending_approval? || @user.registered?
|
|
message_sent = true
|
|
@pseudonym.send_confirmation!
|
|
end
|
|
@user.new_registration((params[:user] || {}).merge({:remote_ip => request.remote_ip, :cookies => cookies}))
|
|
elsif notify && !@user.registered?
|
|
message_sent = true
|
|
@pseudonym.send_registration_notification!
|
|
else
|
|
@cc.send_merge_notification! if @cc.has_merge_candidates?
|
|
end
|
|
|
|
data = { :user => @user, :pseudonym => @pseudonym, :channel => @cc, :observee => @observee, :message_sent => message_sent, :course => @user.self_enrollment_course }
|
|
if api_request?
|
|
render(:json => user_json(@user, @current_user, session, %w{locale}))
|
|
else
|
|
render(:json => data)
|
|
end
|
|
else
|
|
errors = {
|
|
:errors => {
|
|
:user => @user.errors.as_json[:errors],
|
|
:pseudonym => @pseudonym ? @pseudonym.errors.as_json[:errors] : {},
|
|
:observee => @observee ? @observee.errors.as_json[:errors] : {}
|
|
}
|
|
}
|
|
render :json => errors, :status => :bad_request
|
|
end
|
|
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).
|
|
#
|
|
# @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? })
|
|
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
|
|
|
|
respond_to do |format|
|
|
format.json {
|
|
if user.save
|
|
render(json: { manual_mark_as_read: user.manual_mark_as_read? })
|
|
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] [Optional, String]
|
|
# The full name of the user. This name will be used by teacher for grading.
|
|
#
|
|
# @argument user[short_name] [Optional, String]
|
|
# User's name as it will be displayed in discussions, messages, and comments.
|
|
#
|
|
# @argument user[sortable_name] [Optional, String]
|
|
# User's name as used to sort alphabetically in lists.
|
|
#
|
|
# @argument user[time_zone] [Optional, 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] [Optional, String]
|
|
# The user's preferred language as a two-letter ISO 639-1 code.
|
|
#
|
|
# @argument user[avatar][token] [Optional, 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] [Optional, 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 = 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
|
|
|
|
managed_attributes = []
|
|
managed_attributes.concat [:name, :short_name, :sortable_name] if @user.grants_right?(@current_user, nil, :rename)
|
|
managed_attributes << :terms_of_use if @user == (@real_current_user || @current_user)
|
|
if @user.grants_right?(@current_user, nil, :manage_user_details)
|
|
managed_attributes.concat([:time_zone, :locale])
|
|
end
|
|
|
|
if @user.grants_right?(@current_user, nil, :update_avatar)
|
|
avatar = params[:user].delete(:avatar)
|
|
|
|
# delete any avatar_image passed, because we only allow updating avatars
|
|
# based on [:avatar][:token].
|
|
params[:user].delete(:avatar_image)
|
|
|
|
managed_attributes << :avatar_image
|
|
if token = avatar.try(:[], :token)
|
|
if av_json = avatar_for_token(@user, token)
|
|
params[:user][:avatar_image] = { :type => av_json['type'],
|
|
:url => av_json['url'] }
|
|
end
|
|
elsif url = avatar.try(:[], :url)
|
|
params[:user][:avatar_image] = { :type => 'external', :url => url }
|
|
end
|
|
end
|
|
|
|
user_params = params[:user].slice(*managed_attributes)
|
|
|
|
if user_params == params[:user]
|
|
# admins can update avatar images even if they are locked
|
|
admin_avatar_update = user_params[:avatar_image] &&
|
|
@user.grants_right?(@current_user, nil, :update_avatar) &&
|
|
@user.grants_right?(@current_user, nil, :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
|
|
|
|
respond_to do |format|
|
|
if @user.update_attributes(user_params)
|
|
if admin_avatar_update
|
|
@user.avatar_state = (old_avatar_state == :locked ? old_avatar_state : 'approved')
|
|
@user.save
|
|
end
|
|
session.delete(:require_terms)
|
|
flash[:notice] = t('user_updated', 'User was successfully updated.')
|
|
format.html { redirect_to user_url(@user) }
|
|
format.json {
|
|
render :json => user_json(@user, @current_user, session, %w{locale avatar_url},
|
|
@current_user.pseudonym.account) }
|
|
else
|
|
format.html { render :action => "edit" }
|
|
format.json { render :json => @user.errors, :status => :bad_request }
|
|
end
|
|
end
|
|
else
|
|
render_unauthorized_action
|
|
end
|
|
end
|
|
|
|
def media_download
|
|
asset = Kaltura::ClientV3.new.media_sources(params[:entryId]).find{|a| a[:fileExt] == params[:type] }
|
|
url = asset && asset[:url]
|
|
if url
|
|
if params[:redirect] == '1'
|
|
redirect_to url
|
|
else
|
|
render :json => { 'url' => url }
|
|
end
|
|
else
|
|
render :status => 404, :text => t('could_not_find_url', "Could not find download URL")
|
|
end
|
|
end
|
|
|
|
def merge
|
|
@user_about_to_go_away = User.find(params[:user_id])
|
|
|
|
if params[:new_user_id] && @true_user = User.find_by_id(params[:new_user_id])
|
|
if @true_user.grants_right?(@current_user, session, :manage_logins) && @user_about_to_go_away.grants_right?(@current_user, session, :manage_logins)
|
|
@user_that_will_still_be_around = @true_user
|
|
else
|
|
@user_that_will_still_be_around = nil
|
|
end
|
|
else
|
|
@user_that_will_still_be_around = @current_user
|
|
end
|
|
|
|
if @user_about_to_go_away && @user_that_will_still_be_around
|
|
UserMerge.from(@user_about_to_go_away).into(@user_that_will_still_be_around)
|
|
@user_that_will_still_be_around.touch
|
|
flash[:notice] = t('user_merge_success', "User merge succeeded! %{first_user} and %{second_user} are now one and the same.", :first_user => @user_that_will_still_be_around.name, :second_user => @user_about_to_go_away.name)
|
|
else
|
|
flash[:error] = t('user_merge_fail', "User merge failed. Please make sure you have proper permission and try again.")
|
|
end
|
|
|
|
if @user_that_will_still_be_around == @current_user
|
|
redirect_to user_profile_url(@current_user)
|
|
elsif @user_that_will_still_be_around
|
|
redirect_to user_url(@user_that_will_still_be_around)
|
|
else
|
|
redirect_to dashboard_url
|
|
end
|
|
end
|
|
|
|
def admin_merge
|
|
@user = User.find(params[:user_id])
|
|
pending_other_error = get_pending_user_and_error(params[:pending_user_id])
|
|
@other_user = User.find_by_id(params[:new_user_id]) if params[:new_user_id].present?
|
|
if authorized_action(@user, @current_user, :manage_logins)
|
|
flash[:error] = pending_other_error if pending_other_error.present?
|
|
|
|
if @user && (params[:clear] || !@pending_other_user)
|
|
@pending_other_user = nil
|
|
end
|
|
|
|
unless @other_user && @other_user.grants_right?(@current_user, session, :manage_logins)
|
|
@other_user = nil
|
|
end
|
|
|
|
render :action => 'admin_merge'
|
|
end
|
|
end
|
|
|
|
def get_pending_user_and_error(pending_user_id)
|
|
pending_other_error = nil
|
|
|
|
if pending_user_id.present?
|
|
@pending_other_user = api_find_all(User, [pending_user_id]).first
|
|
@pending_other_user = nil unless @pending_other_user.try(:grants_right?, @current_user, session, :manage_logins)
|
|
if @pending_other_user == @user
|
|
@pending_other_user = nil
|
|
pending_other_error = t('cant_self_merge', "You can't merge an account with itself.")
|
|
elsif @pending_other_user.blank? && pending_other_error.blank?
|
|
pending_other_error = t('user_not_found', "No active user with that ID was found.")
|
|
end
|
|
end
|
|
|
|
pending_other_error
|
|
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.find_by_id(@user.id)
|
|
end
|
|
@user.report_avatar_image!(@context)
|
|
end
|
|
session["reports_#{@user.id}".to_sym] = true
|
|
render :json => {:reported => true}
|
|
end
|
|
end
|
|
|
|
def delete
|
|
@user = User.find(params[:user_id])
|
|
if authorized_action(@user, @current_user, [:manage, :manage_logins])
|
|
if @user.pseudonyms.any? {|p| p.managed_password? }
|
|
unless @user.grants_right?(@current_user, session, :manage_logins)
|
|
flash[:error] = t('no_deleting_sis_user', "You cannot delete a system-generated user")
|
|
redirect_to user_profile_url(@current_user)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
# @API Delete a user
|
|
#
|
|
# Delete a user record from Canvas.
|
|
#
|
|
# WARNING: This API will allow a user to delete themselves. If you do this,
|
|
# you won't be able to make API calls or log into Canvas.
|
|
#
|
|
# @example_request
|
|
# curl https://<canvas>/api/v1/users/5 \
|
|
# -H 'Authorization: Bearer <ACCESS_TOKEN>' \
|
|
# -X DELETE
|
|
#
|
|
# @returns User
|
|
def destroy
|
|
@user = api_find(User, params[:id])
|
|
if authorized_action(@user, @current_user, [:manage, :manage_logins])
|
|
@user.destroy(@user.grants_right?(@current_user, session, :manage_logins))
|
|
if @user == @current_user
|
|
logout_current_user
|
|
end
|
|
|
|
respond_to do |format|
|
|
format.html do
|
|
flash[:notice] = t('user_is_deleted', "%{user_name} has been deleted", :user_name => @user.name)
|
|
redirect_to(@user == @current_user ? root_url : users_url)
|
|
end
|
|
|
|
format.json do
|
|
get_context # need the context for user_json
|
|
render :json => user_json(@user, @current_user, session)
|
|
end
|
|
end
|
|
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 :text => feed.to_xml }
|
|
end
|
|
end
|
|
|
|
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
|
|
|
|
def all_menu_courses
|
|
render :json => Rails.cache.fetch(['menu_courses', @current_user].cache_key) {
|
|
map_courses_for_menu(@current_user.courses_with_primary_enrollment)
|
|
}
|
|
end
|
|
|
|
protected :require_self_registration
|
|
|
|
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.includes(:course).all
|
|
enrollments.each do |enrollment|
|
|
should_include = enrollment.course.user_has_been_instructor?(@teacher) &&
|
|
enrollment.course.enrollments_visible_to(@teacher, :include_priors => true).find_by_id(enrollment.id) &&
|
|
enrollment.course.grants_right?(@current_user, :read_reports)
|
|
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)
|
|
@courses[course] = teacher_activity_report(@teacher, course, course.enrollments_visible_to(@teacher, :include_priors => true))
|
|
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(params[:fallback] || '/images/no_pic.gif') 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'
|
|
url = Rails.cache.fetch(Cacher.avatar_cache_key(user_id, account_avatar_setting)) do
|
|
user = User.find_by_id(user_id) if user_id.present?
|
|
if user
|
|
user.avatar_url(nil, account_avatar_setting, "%{fallback}")
|
|
else
|
|
'%{fallback}'
|
|
end
|
|
end
|
|
fallback = User.avatar_fallback_url(params[:fallback], request)
|
|
redirect_to (url.blank? || url == "%{fallback}") ?
|
|
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.
|
|
#
|
|
# 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, :manage_logins)
|
|
|
|
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, :manage_logins)
|
|
UserMerge.from(user).into into_user
|
|
render(:json => user_json(into_user,
|
|
@current_user,
|
|
session,
|
|
%w{locale},
|
|
destination_account))
|
|
end
|
|
end
|
|
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 = SubmissionComment.for_context(course).
|
|
group(:recipient_id).
|
|
where("author_id = ? AND recipient_id IN (?)", teacher, ids).
|
|
maximum(:created_at)
|
|
last_comment_dates.each do |user_id, date|
|
|
next unless student = data[user_id]
|
|
student[:last_interaction] = [student[:last_interaction], date].compact.max
|
|
end
|
|
scope = ConversationMessage.
|
|
joins('INNER JOIN conversation_participants 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 = CANVAS_RAILS2 ?
|
|
scope.maximum(:created_at, :group => ['conversation_participants.user_id', 'conversation_messages.author_id']) :
|
|
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.
|
|
includes(:assignment).
|
|
where("user_id IN (?) AND #{Submission.needs_grading_conditions}", ids).
|
|
all
|
|
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
|
|
end
|