canvas-lms/app/controllers/profile_controller.rb

469 lines
18 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/>.
#
# @API Users
#
# @model Profile
# {
# "id": "Profile",
# "description": "Profile details for a Canvas user.",
# "properties": {
# "id": {
# "description": "The ID of the user.",
# "example": 1234,
# "type": "integer"
# },
# "name": {
# "description": "Sample User",
# "example": "Sample User",
# "type": "string"
# },
# "short_name": {
# "description": "Sample User",
# "example": "Sample User",
# "type": "string"
# },
# "sortable_name": {
# "description": "user, sample",
# "example": "user, sample",
# "type": "string"
# },
# "title": {
# "type": "string"
# },
# "bio": {
# "type": "string"
# },
# "primary_email": {
# "description": "sample_user@example.com",
# "example": "sample_user@example.com",
# "type": "string"
# },
# "login_id": {
# "description": "sample_user@example.com",
# "example": "sample_user@example.com",
# "type": "string"
# },
# "sis_user_id": {
# "description": "sis1",
# "example": "sis1",
# "type": "string"
# },
# "sis_login_id": {
# "description": "sis1-login",
# "example": "sis1-login",
# "type": "string"
# },
# "lti_user_id": {
# "type": "string"
# },
# "avatar_url": {
# "description": "The avatar_url can change over time, so we recommend not caching it for more than a few hours",
# "example": "..url..",
# "type": "string"
# },
# "calendar": {
# "$ref": "CalendarLink"
# },
# "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"
# },
# "locale": {
# "description": "The users locale.",
# "type": "string"
# }
# }
# }
#
# @model Avatar
# {
# "id": "Avatar",
# "description": "Possible avatar for a user.",
# "required": ["type", "url", "token", "display_name"],
# "properties": {
# "type": {
# "description": "['gravatar'|'attachment'|'no_pic'] The type of avatar record, for categorization purposes.",
# "example": "gravatar",
# "type": "string"
# },
# "url": {
# "description": "The url of the avatar",
# "example": "https://secure.gravatar.com/avatar/2284...",
# "type": "string"
# },
# "token": {
# "description": "A unique representation of the avatar record which can be used to set the avatar with the user update endpoint. 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.",
# "example": "<opaque_token>",
# "type": "string"
# },
# "display_name": {
# "description": "A textual description of the avatar record.",
# "example": "user, sample",
# "type": "string"
# },
# "id": {
# "description": "['attachment' type only] the internal id of the attachment",
# "example": 12,
# "type": "integer"
# },
# "content-type": {
# "description": "['attachment' type only] the content-type of the attachment.",
# "example": "image/jpeg",
# "type": "string"
# },
# "filename": {
# "description": "['attachment' type only] the filename of the attachment",
# "example": "profile.jpg",
# "type": "string"
# },
# "size": {
# "description": "['attachment' type only] the size of the attachment",
# "example": 32649,
# "type": "integer"
# }
# }
# }
#
class ProfileController < ApplicationController
before_action :require_registered_user, :except => [:show, :settings, :communication, :communication_update]
before_action :require_user, :only => [:settings, :communication, :communication_update]
before_action :require_user_for_private_profile, :only => :show
before_action :reject_student_view_student
before_action :require_password_session, :only => [:communication, :communication_update, :update]
include Api::V1::Avatar
include Api::V1::CommunicationChannel
include Api::V1::NotificationPolicy
include Api::V1::UserProfile
include TextHelper
def show
unless @current_user && @domain_root_account.enable_profiles?
return unless require_password_session
settings
return
end
@user ||= @current_user
@active_tab = "profile"
@context = @user.profile if @user == @current_user
@user_data = profile_data(
@user.profile,
@current_user,
session,
['links', 'user_services']
)
if @user_data[:known_user] # if you can message them, you can see the profile
add_crumb(t('crumbs.settings_frd', "%{user}'s Profile", :user => @user.short_name), user_profile_path(@user))
render
else
render :unauthorized
end
end
# @API Get user profile
# Returns user profile data, including user id, name, and profile pic.
#
# When requesting the profile for the user accessing the API, the user's
# calendar feed URL and LTI user id will be returned as well.
#
# @returns Profile
def settings
if api_request?
@user = api_find(User, params[:user_id])
return unless authorized_action(@user, @current_user, :read_profile)
else
return unless require_password_session
@user = @current_user
@user.dismiss_bouncing_channel_message!
end
@user_data = profile_data(@user.profile, @current_user, session, [])
@channels = @user.communication_channels.unretired
@email_channels = @channels.select{|c| c.path_type == "email"}
@sms_channels = @channels.select{|c| c.path_type == 'sms'}
@other_channels = @channels.select{|c| c.path_type != "email"}
@default_email_channel = @email_channels.first
@default_pseudonym = @user.primary_pseudonym
@pseudonyms = @user.pseudonyms.active
@password_pseudonyms = @pseudonyms.select{|p| !p.managed_password? }
@context = @user.profile
@active_tab = "profile_settings"
respond_to do |format|
format.html do
show_tutorial_ff_to_user = @domain_root_account&.feature_enabled?(:new_user_tutorial) &&
@user.participating_instructor_course_ids.any?
add_crumb(t(:crumb, "%{user}'s settings", :user => @user.short_name), settings_profile_path )
js_env(:NEW_USER_TUTORIALS_ENABLED_AT_ACCOUNT => show_tutorial_ff_to_user)
render :profile
end
format.json do
render :json => user_profile_json(@user.profile, @current_user, session, params[:include])
end
end
end
def communication
@user = @current_user
@current_user.used_feature(:cc_prefs)
@context = @user.profile
@active_tab = 'notifications'
# Get the list of Notification models (that are treated like categories) that make up the full list of Categories.
full_category_list = Notification.dashboard_categories(@user)
categories = full_category_list.map do |category|
category.as_json(only: %w{id name workflow_state user_id}, include_root: false).tap do |json|
# Add custom method result entries to the json
json[:category] = category.category.underscore.gsub(/\s/, '_')
json[:display_name] = category.category_display_name
json[:category_description] = category.category_description
json[:option] = category.related_user_setting(@user, @domain_root_account)
end
end
js_env :NOTIFICATION_PREFERENCES_OPTIONS => {
:channels => @user.communication_channels.all_ordered_for_display(@user).map { |c| communication_channel_json(c, @user, session) },
:policies => NotificationPolicy.setup_with_default_policies(@user, full_category_list).map { |p| notification_policy_json(p, @user, session).tap { |json| json[:communication_channel_id] = p.communication_channel_id } },
:categories => categories,
:update_url => communication_update_profile_path,
},
:READ_PRIVACY_INFO => @user.preferences[:read_notification_privacy_info],
:ACCOUNT_PRIVACY_NOTICE => @domain_root_account.settings[:external_notification_warning]
end
def communication_update
params[:root_account] = @domain_root_account
NotificationPolicy.setup_for(@current_user, params)
render :json => {}, :status => :ok
end
# @API List avatar options
# Retrieve the possible user avatar options that can be set with the user update endpoint. The response will be an array of avatar records. If the 'type' field is 'attachment', the record will include all the normal attachment json fields; otherwise it will include only the 'url' and 'display_name' fields. Additionally, all records will include a 'type' field and a 'token' field. The following explains each field in more detail
# type:: ["gravatar"|"attachment"|"no_pic"] The type of avatar record, for categorization purposes.
# url:: The url of the avatar
# token:: A unique representation of the avatar record which can be used to set the avatar with the user update endpoint. 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.
# display_name:: A textual description of the avatar record
# id:: ['attachment' type only] the internal id of the attachment
# content-type:: ['attachment' type only] the content-type of the attachment
# filename:: ['attachment' type only] the filename of the attachment
# size:: ['attachment' type only] the size of the attachment
#
# @example_request
#
# curl 'https://<canvas>/api/v1/users/1/avatars.json' \
# -H "Authorization: Bearer <token>"
#
# @example_response
#
# [
# {
# "type":"gravatar",
# "url":"https://secure.gravatar.com/avatar/2284...",
# "token":<opaque_token>,
# "display_name":"gravatar pic"
# },
# {
# "type":"attachment",
# "url":"https://<canvas>/images/thumbnails/12/gpLWJ...",
# "token":<opaque_token>,
# "display_name":"profile.jpg",
# "id":12,
# "content-type":"image/jpeg",
# "filename":"profile.jpg",
# "size":32649
# },
# {
# "type":"no_pic",
# "url":"https://<canvas>/images/dotted_pic.png",
# "token":<opaque_token>,
# "display_name":"no pic"
# }
# ]
# @returns [Avatar]
def profile_pics
@user = if api_request? then api_find(User, params[:user_id]) else @current_user end
if authorized_action(@user, @current_user, :update_avatar)
render :json => avatars_json_for_user(@user)
end
end
def toggle_disable_inbox
disable_inbox = value_to_boolean(params[:user][:disable_inbox])
@current_user.preferences[:disable_inbox] = disable_inbox
@current_user.save!
email_channel_id = @current_user.email_channel.try(:id)
if disable_inbox && !email_channel_id.nil?
params = {:channel_id=>email_channel_id,:frequency=>"immediately"}
["added_to_conversation", "conversation_message"].each do |category|
params[:category] = category
NotificationPolicy.setup_for(@current_user, params)
end
end
render :json => {}
end
def update
@user = @current_user
if params[:privacy_notice].present?
@user.preferences[:read_notification_privacy_info] = Time.now.utc.to_s
@user.save
return head 208
end
respond_to do |format|
user_params = params[:user] ? params[:user].
permit(:name, :short_name, :sortable_name, :time_zone, :show_user_services, :gender,
:avatar_image, :subscribe_to_emails, :locale, :bio, :birthdate)
: {}
if !@user.user_can_edit_name?
user_params.delete(:name)
user_params.delete(:short_name)
user_params.delete(:sortable_name)
end
if @user.update_attributes(user_params)
pseudonymed = false
if params[:default_email_id].present?
@email_channel = @user.communication_channels.email.where(id: params[:default_email_id]).first
if @email_channel
@email_channel.move_to_top
@user.clear_email_cache!
end
end
if params[:pseudonym]
pseudonym_params = params[:pseudonym].permit(:password, :password_confirmation, :unique_id)
change_password = params[:pseudonym].delete :change_password
old_password = params[:pseudonym].delete :old_password
if params[:pseudonym][:password_id] && change_password
pseudonym_to_update = @user.pseudonyms.find(params[:pseudonym][:password_id])
pseudonym_to_update.require_password = true if pseudonym_to_update
end
if change_password == '1' && pseudonym_to_update && !pseudonym_to_update.valid_arbitrary_credentials?(old_password)
error_msg = t('errors.invalid_old_passowrd', "Invalid old password for the login %{pseudonym}", :pseudonym => pseudonym_to_update.unique_id)
pseudonymed = true
flash[:error] = error_msg
format.html { redirect_to user_profile_url(@current_user) }
format.json { render :json => {:errors => {:old_password => error_msg}}, :status => :bad_request }
end
if change_password != '1' || !pseudonym_to_update || !pseudonym_to_update.valid_arbitrary_credentials?(old_password)
pseudonym_params.delete :password
pseudonym_params.delete :password_confirmation
end
params[:pseudonym].delete :password_id
if !pseudonym_params.empty? && pseudonym_to_update && !pseudonym_to_update.update_attributes(pseudonym_params)
pseudonymed = true
flash[:error] = t('errors.profile_update_failed', "Login failed to update")
format.html { redirect_to user_profile_url(@current_user) }
format.json { render :json => pseudonym_to_update.errors, :status => :bad_request }
end
end
unless pseudonymed
flash[:notice] = t('notices.updated_profile', "Settings successfully updated")
format.html { redirect_to user_profile_url(@current_user) }
format.json { render :json => @user.as_json(:methods => :avatar_url, :include => {:communication_channel => {:only => [:id, :path], :include_root => false}, :pseudonym => {:only => [:id, :unique_id], :include_root => false} }) }
end
else
format.html
format.json { render :json => @user.errors }
end
end
end
# TODO: the current update method needs to get moved to the UsersController
# (since it is not concerned with profiles), then this should get renamed
#
# not doing API docs until we can move this to PUT /profile
def update_profile
@user = @current_user
@profile = @user.profile
@context = @profile
short_name = params[:user] && params[:user][:short_name]
@user.short_name = short_name if short_name && @user.user_can_edit_name?
if params[:user_profile]
user_profile_params = params[:user_profile].permit(:title, :bio)
user_profile_params.delete(:title) unless @user.user_can_edit_name?
@profile.attributes = user_profile_params
end
if params[:link_urls] && params[:link_titles]
@profile.links = []
params[:link_urls].zip(params[:link_titles]).
reject { |url, title| url.blank? && title.blank? }.
each { |url, title|
@profile.links.build :url => url, :title => title
}
elsif params[:delete_links]
@profile.links = []
end
if @user.valid? && @profile.valid?
@user.save!
@profile.save!
if params[:user_services]
visible, invisible = params[:user_services].partition { |service,bool|
value_to_boolean(bool)
}
@user.user_services.where(:service => visible.map(&:first)).update_all(:visible => true)
@user.user_services.where(:service => invisible.map(&:first)).update_all(:visible => false)
end
respond_to do |format|
format.html { redirect_to user_profile_path(@user) }
format.json { render :json => user_profile_json(@user.profile, @current_user, session, params[:includes]) }
end
else
respond_to do |format|
format.html { redirect_to user_profile_path(@user) } # FIXME: need to go to edit path
format.json { render :json => @profile.errors, :status => :bad_request } #NOTE: won't send back @user validation errors (i.e. short_name)
end
end
end
def require_user_for_private_profile
if params[:id]
@user = api_find(User, params[:id])
return if @user.public?
end
require_user
end
private :require_user_for_private_profile
def observees
if @domain_root_account.parent_registration?
js_env(AUTH_TYPE: @domain_root_account.parent_auth_type)
end
@user ||= @current_user
@active_tab = 'observees'
@context = @user.profile if @user == @current_user
add_crumb(@user.short_name, profile_path)
add_crumb(t('crumbs.observees', "Observing"))
end
end