905 lines
37 KiB
Ruby
905 lines
37 KiB
Ruby
#
|
|
# Copyright (C) 2011 - 2014 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 'csv'
|
|
|
|
# @API Accounts
|
|
#
|
|
# API for accessing account data.
|
|
#
|
|
# @model Account
|
|
# {
|
|
# "id": "Account",
|
|
# "description": "",
|
|
# "properties": {
|
|
# "id": {
|
|
# "description": "the ID of the Account object",
|
|
# "example": 2,
|
|
# "type": "integer"
|
|
# },
|
|
# "name": {
|
|
# "description": "The display name of the account",
|
|
# "example": "Canvas Account",
|
|
# "type": "string"
|
|
# },
|
|
# "parent_account_id": {
|
|
# "description": "The account's parent ID, or null if this is the root account",
|
|
# "example": 1,
|
|
# "type": "integer"
|
|
# },
|
|
# "root_account_id": {
|
|
# "description": "The ID of the root account, or null if this is the root account",
|
|
# "example": 1,
|
|
# "type": "integer"
|
|
# },
|
|
# "default_storage_quota_mb": {
|
|
# "description": "The storage quota for the account in megabytes, if not otherwise specified",
|
|
# "example": 500,
|
|
# "type": "integer"
|
|
# },
|
|
# "default_user_storage_quota_mb": {
|
|
# "description": "The storage quota for a user in the account in megabytes, if not otherwise specified",
|
|
# "example": 50,
|
|
# "type": "integer"
|
|
# },
|
|
# "default_group_storage_quota_mb": {
|
|
# "description": "The storage quota for a group in the account in megabytes, if not otherwise specified",
|
|
# "example": 50,
|
|
# "type": "integer"
|
|
# },
|
|
# "default_time_zone": {
|
|
# "description": "The default time zone of the account. 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}.",
|
|
# "example": "America/Denver",
|
|
# "type": "string"
|
|
# },
|
|
# "sis_account_id": {
|
|
# "description": "The account's identifier in the Student Information System. Only included if the user has permission to view SIS information.",
|
|
# "example": "123xyz",
|
|
# "type": "string"
|
|
# },
|
|
# "integration_id": {
|
|
# "description": "The account's identifier in the Student Information System. Only included if the user has permission to view SIS information.",
|
|
# "example": "123xyz",
|
|
# "type": "string"
|
|
# },
|
|
# "sis_import_id": {
|
|
# "description": "The id of the SIS import if created through SIS. Only included if the user has permission to manage SIS information.",
|
|
# "example": "12",
|
|
# "type": "integer"
|
|
# },
|
|
# "lti_guid": {
|
|
# "description": "The account's identifier that is sent as context_id in LTI launches.",
|
|
# "example": "123xyz",
|
|
# "type": "string"
|
|
# },
|
|
# "workflow_state": {
|
|
# "description": "The state of the account. Can be 'active' or 'deleted'.",
|
|
# "example": "active",
|
|
# "type": "string"
|
|
# }
|
|
# }
|
|
# }
|
|
#
|
|
class AccountsController < ApplicationController
|
|
before_filter :require_user, :only => [:index]
|
|
before_filter :reject_student_view_student
|
|
before_filter :get_context
|
|
|
|
include Api::V1::Account
|
|
|
|
INTEGER_REGEX = /\A[+-]?\d+\z/
|
|
|
|
# @API List accounts
|
|
# List accounts that the current user can view or manage. Typically,
|
|
# students and even teachers will get an empty list in response, only
|
|
# account admins can view the accounts that they are in.
|
|
#
|
|
# @argument include[] [String, "lti_guid"|"registration_settings"]
|
|
# Array of additional information to include.
|
|
#
|
|
# "lti_guid":: the 'tool_consumer_instance_guid' that will be sent for this account on LTI launches
|
|
# "registration_settings":: returns info about the privacy policy and terms of use
|
|
#
|
|
# @returns [Account]
|
|
def index
|
|
respond_to do |format|
|
|
format.html do
|
|
@accounts = @current_user ? @current_user.all_accounts : []
|
|
end
|
|
format.json do
|
|
if @current_user
|
|
@accounts = Api.paginate(@current_user.all_paginatable_accounts, self, api_v1_accounts_url)
|
|
else
|
|
@accounts = []
|
|
end
|
|
ActiveRecord::Associations::Preloader.new(@accounts, :root_account).run
|
|
|
|
# originally had 'includes' instead of 'include' like other endpoints
|
|
includes = params[:include] || params[:includes]
|
|
render :json => @accounts.map { |a| account_json(a, @current_user, session, includes || [], false) }
|
|
end
|
|
end
|
|
end
|
|
|
|
# @API List accounts for course admins
|
|
# List accounts that the current user can view through their admin course enrollments.
|
|
# (Teacher, TA, or designer enrollments).
|
|
# Only returns "id", "name", "workflow_state", "root_account_id" and "parent_account_id"
|
|
#
|
|
# @returns [Account]
|
|
def course_accounts
|
|
if @current_user
|
|
account_ids = Rails.cache.fetch(['admin_enrollment_course_account_ids', @current_user].cache_key) do
|
|
Account.joins(:courses => :enrollments).merge(
|
|
@current_user.enrollments.admin.shard(@current_user).except(:select)
|
|
).select("accounts.id").uniq.pluck(:id).map{|id| Shard.global_id_for(id)}
|
|
end
|
|
course_accounts = BookmarkedCollection.wrap(Account::Bookmarker, Account.where(:id => account_ids))
|
|
@accounts = Api.paginate(course_accounts, self, api_v1_accounts_url)
|
|
else
|
|
@accounts = []
|
|
end
|
|
ActiveRecord::Associations::Preloader.new(@accounts, :root_account).run
|
|
render :json => @accounts.map { |a| account_json(a, @current_user, session, params[:includes] || [], true) }
|
|
end
|
|
|
|
# @API Get a single account
|
|
# Retrieve information on an individual account, given by id or sis
|
|
# sis_account_id.
|
|
#
|
|
# @returns Account
|
|
def show
|
|
return unless authorized_action(@account, @current_user, :read)
|
|
respond_to do |format|
|
|
format.html do
|
|
if value_to_boolean(params[:theme_applied])
|
|
flash[:notice] = t("Your custom theme has been successfully applied.")
|
|
end
|
|
return redirect_to account_settings_url(@account) if @account.site_admin? || !@account.grants_right?(@current_user, :read_course_list)
|
|
js_env(:ACCOUNT_COURSES_PATH => account_courses_path(@account, :format => :json))
|
|
load_course_right_side
|
|
@courses = @account.fast_all_courses(:term => @term, :limit => @maximum_courses_im_gonna_show, :hide_enrollmentless_courses => @hide_enrollmentless_courses)
|
|
ActiveRecord::Associations::Preloader.new(@courses, :enrollment_term).run
|
|
build_course_stats
|
|
end
|
|
format.json { render :json => account_json(@account, @current_user, session, params[:includes] || [],
|
|
!@account.grants_right?(@current_user, session, :manage)) }
|
|
end
|
|
end
|
|
|
|
# @API Get the sub-accounts of an account
|
|
#
|
|
# List accounts that are sub-accounts of the given account.
|
|
#
|
|
# @argument recursive [Boolean] If true, the entire account tree underneath
|
|
# this account will be returned (though still paginated). If false, only
|
|
# direct sub-accounts of this account will be returned. Defaults to false.
|
|
#
|
|
# @example_request
|
|
# curl https://<canvas>/api/v1/accounts/<account_id>/sub_accounts \
|
|
# -H 'Authorization: Bearer <token>'
|
|
#
|
|
# @returns [Account]
|
|
def sub_accounts
|
|
return unless authorized_action(@account, @current_user, :read)
|
|
recursive = value_to_boolean(params[:recursive])
|
|
if recursive
|
|
@accounts = PaginatedCollection.build do |pager|
|
|
per_page = pager.per_page
|
|
current_page = [pager.current_page.to_i, 1].max
|
|
sub_accounts = @account.sub_accounts_recursive(per_page + 1, (current_page - 1) * per_page)
|
|
|
|
if sub_accounts.length > per_page
|
|
sub_accounts.pop
|
|
pager.next_page = current_page + 1
|
|
end
|
|
|
|
pager.replace sub_accounts
|
|
end
|
|
else
|
|
@accounts = @account.sub_accounts.order(:id)
|
|
end
|
|
|
|
@accounts = Api.paginate(@accounts, self, api_v1_sub_accounts_url,
|
|
:total_entries => recursive ? nil : @accounts.count)
|
|
|
|
ActiveRecord::Associations::Preloader.new(@accounts, [:root_account, :parent_account]).run
|
|
render :json => @accounts.map { |a| account_json(a, @current_user, session, []) }
|
|
end
|
|
|
|
include Api::V1::Course
|
|
|
|
# @API List active courses in an account
|
|
# Retrieve the list of courses in this account.
|
|
#
|
|
# @argument with_enrollments [Boolean]
|
|
# If true, include only courses with at least one enrollment. If false,
|
|
# include only courses with no enrollments. If not present, do not filter
|
|
# on course enrollment status.
|
|
#
|
|
# @argument published [Boolean]
|
|
# If true, include only published courses. If false, exclude published
|
|
# courses. If not present, do not filter on published status.
|
|
#
|
|
# @argument completed [Boolean]
|
|
# If true, include only completed courses (these may be in state
|
|
# 'completed', or their enrollment term may have ended). If false, exclude
|
|
# completed courses. If not present, do not filter on completed status.
|
|
#
|
|
# @argument by_teachers[] [Integer]
|
|
# List of User IDs of teachers; if supplied, include only courses taught by
|
|
# one of the referenced users.
|
|
#
|
|
# @argument by_subaccounts[] [Integer]
|
|
# List of Account IDs; if supplied, include only courses associated with one
|
|
# of the referenced subaccounts.
|
|
#
|
|
# @argument hide_enrollmentless_courses [Boolean]
|
|
# If present, only return courses that have at least one enrollment.
|
|
# Equivalent to 'with_enrollments=true'; retained for compatibility.
|
|
#
|
|
# @argument state[] ["created"|"claimed"|"available"|"completed"|"deleted"|"all"]
|
|
# If set, only return courses that are in the given state(s). By default,
|
|
# all states but "deleted" are returned.
|
|
#
|
|
# @argument enrollment_term_id [Integer]
|
|
# If set, only includes courses from the specified term.
|
|
#
|
|
# @argument search_term [String]
|
|
# The partial course name, code, or full ID to match and return in the results list. Must be at least 3 characters.
|
|
#
|
|
# @argument include[] [String, "syllabus_body"|"term"|"course_progress"|"storage_quota_used_mb"]
|
|
# - All explanations can be seen in the {api:CoursesController#index Course API index documentation}
|
|
# - "sections", "needs_grading_count" and "total_scores" are not valid options at the account level
|
|
#
|
|
# @returns [Course]
|
|
def courses_api
|
|
return unless authorized_action(@account, @current_user, :read)
|
|
|
|
params[:state] ||= %w{created claimed available completed}
|
|
params[:state] = %w{created claimed available completed deleted} if Array(params[:state]).include?('all')
|
|
if value_to_boolean(params[:published])
|
|
params[:state] -= %w{created claimed completed deleted}
|
|
elsif !params[:published].nil? && !value_to_boolean(params[:published])
|
|
params[:state] -= %w{available}
|
|
end
|
|
|
|
@courses = @account.associated_courses.order(:id).where(:workflow_state => params[:state])
|
|
if params[:hide_enrollmentless_courses] || value_to_boolean(params[:with_enrollments])
|
|
@courses = @courses.with_enrollments
|
|
elsif !params[:with_enrollments].nil? && !value_to_boolean(params[:with_enrollments])
|
|
@courses = @courses.without_enrollments
|
|
end
|
|
|
|
if value_to_boolean(params[:completed])
|
|
@courses = @courses.completed
|
|
elsif !params[:completed].nil? && !value_to_boolean(params[:completed])
|
|
@courses = @courses.not_completed
|
|
end
|
|
|
|
if params[:by_teachers].is_a?(Array)
|
|
teacher_ids = Api.map_ids(params[:by_teachers], User, @domain_root_account, @current_user).map(&:to_i)
|
|
@courses = @courses.by_teachers(teacher_ids)
|
|
end
|
|
|
|
if params[:by_subaccounts].is_a?(Array)
|
|
account_ids = Api.map_ids(params[:by_subaccounts], Account, @domain_root_account, @current_user).map(&:to_i)
|
|
@courses = @courses.by_associated_accounts(account_ids)
|
|
end
|
|
|
|
if params[:enrollment_term_id]
|
|
term = api_find(@account.root_account.enrollment_terms, params[:enrollment_term_id])
|
|
@courses = @courses.for_term(term)
|
|
end
|
|
|
|
if params[:search_term]
|
|
search_term = params[:search_term]
|
|
|
|
is_id = search_term.to_s =~ Api::ID_REGEX
|
|
if is_id && course = @courses.where(id: search_term).first
|
|
@courses = [course]
|
|
elsif is_id && !SearchTermHelper.valid_search_term?(search_term)
|
|
@courses = []
|
|
else
|
|
SearchTermHelper.validate_search_term(search_term)
|
|
|
|
name = ActiveRecord::Base.wildcard('courses.name', search_term)
|
|
code = ActiveRecord::Base.wildcard('courses.course_code', search_term)
|
|
@courses = @courses.where("#{name} OR #{code}")
|
|
end
|
|
end
|
|
|
|
includes = Set.new(Array(params[:include]))
|
|
# We only want to return the permissions for single courses and not lists of courses.
|
|
# sections, needs_grading_count, and total_score not valid as enrollments are needed
|
|
includes -= ['permissions', 'sections', 'needs_grading_count', 'total_scores']
|
|
|
|
@courses = Api.paginate(@courses, self, api_v1_account_courses_url)
|
|
|
|
ActiveRecord::Associations::Preloader.new(@courses, [:account, :root_account]).run
|
|
render :json => @courses.map { |c| course_json(c, @current_user, session, includes, nil) }
|
|
end
|
|
|
|
# Delegated to by the update action (when the request is an api_request?)
|
|
def update_api
|
|
if authorized_action(@account, @current_user, [:manage_account_settings, :manage_storage_quotas])
|
|
account_params = params[:account] || {}
|
|
unauthorized = false
|
|
|
|
# account settings (:manage_account_settings)
|
|
account_settings = account_params.select {|k, v| [:name, :default_time_zone].include?(k.to_sym)}.with_indifferent_access
|
|
unless account_settings.empty?
|
|
if @account.grants_right?(@current_user, session, :manage_account_settings)
|
|
@account.errors.add(:name, t(:account_name_required, 'The account name cannot be blank')) if account_params.has_key?(:name) && account_params[:name].blank?
|
|
@account.errors.add(:default_time_zone, t(:unrecognized_time_zone, "'%{timezone}' is not a recognized time zone", :timezone => account_params[:default_time_zone])) if account_params.has_key?(:default_time_zone) && ActiveSupport::TimeZone.new(account_params[:default_time_zone]).nil?
|
|
else
|
|
account_settings.each {|k, v| @account.errors.add(k.to_sym, t(:cannot_manage_account, 'You are not allowed to manage account settings'))}
|
|
unauthorized = true
|
|
end
|
|
end
|
|
|
|
# quotas (:manage_account_quotas)
|
|
quota_settings = account_params.select {|k, v| [:default_storage_quota_mb, :default_user_storage_quota_mb,
|
|
:default_group_storage_quota_mb].include?(k.to_sym)}.with_indifferent_access
|
|
unless quota_settings.empty?
|
|
if @account.grants_right?(@current_user, session, :manage_storage_quotas)
|
|
[:default_storage_quota_mb, :default_user_storage_quota_mb, :default_group_storage_quota_mb].each do |quota_type|
|
|
next unless quota_settings.has_key?(quota_type)
|
|
|
|
quota_value = quota_settings[quota_type].to_s.strip
|
|
if INTEGER_REGEX !~ quota_value.to_s
|
|
@account.errors.add(quota_type, t(:quota_integer_required, 'An integer value is required'))
|
|
else
|
|
@account.errors.add(quota_type, t(:quota_must_be_positive, 'Value must be positive')) if quota_value.to_i < 0
|
|
@account.errors.add(quota_type, t(:quota_too_large, 'Value too large')) if quota_value.to_i >= 2**62 / 1.megabytes
|
|
end
|
|
end
|
|
else
|
|
quota_settings.each {|k, v| @account.errors.add(k.to_sym, t(:cannot_manage_quotas, 'You are not allowed to manage quota settings'))}
|
|
unauthorized = true
|
|
end
|
|
end
|
|
|
|
if unauthorized
|
|
# Attempt to modify something without sufficient permissions
|
|
render :json => @account.errors, :status => :unauthorized
|
|
else
|
|
success = @account.errors.empty?
|
|
success &&= @account.update_attributes(account_settings.merge(quota_settings)) rescue false
|
|
|
|
if success
|
|
# Successfully completed
|
|
render :json => account_json(@account, @current_user, session, params[:includes] || [])
|
|
else
|
|
# Failed (hopefully with errors)
|
|
render :json => @account.errors, :status => :bad_request
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
# @API Update an account
|
|
# Update an existing account.
|
|
#
|
|
# @argument account[name] [String]
|
|
# Updates the account name
|
|
#
|
|
# @argument account[default_time_zone] [String]
|
|
# The default time zone of the account. 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 account[default_storage_quota_mb] [Integer]
|
|
# The default course storage quota to be used, if not otherwise specified.
|
|
#
|
|
# @argument account[default_user_storage_quota_mb] [Integer]
|
|
# The default user storage quota to be used, if not otherwise specified.
|
|
#
|
|
# @argument account[default_group_storage_quota_mb] [Integer]
|
|
# The default group storage quota to be used, if not otherwise specified.
|
|
#
|
|
# @argument account[settings][restrict_student_past_view] [Boolean]
|
|
# Restrict students from viewing courses after end date
|
|
#
|
|
# @argument account[settings][restrict_student_future_view] [Boolean]
|
|
# Restrict students from viewing courses before start date
|
|
#
|
|
# @example_request
|
|
# curl https://<canvas>/api/v1/accounts/<account_id> \
|
|
# -X PUT \
|
|
# -H 'Authorization: Bearer <token>' \
|
|
# -d 'account[name]=New account name' \
|
|
# -d 'account[default_time_zone]=Mountain Time (US & Canada)' \
|
|
# -d 'account[default_storage_quota_mb]=450'
|
|
#
|
|
# @returns Account
|
|
def update
|
|
return update_api if api_request?
|
|
|
|
if authorized_action(@account, @current_user, :manage_account_settings)
|
|
respond_to do |format|
|
|
|
|
custom_help_links = params[:account].delete :custom_help_links
|
|
if custom_help_links
|
|
@account.settings[:custom_help_links] = custom_help_links.select{|k, h| h['state'] != 'deleted'}.sort.map do |index_with_hash|
|
|
hash = index_with_hash[1]
|
|
hash.delete('state')
|
|
hash.assert_valid_keys ["text", "subtext", "url", "available_to"]
|
|
hash
|
|
end
|
|
end
|
|
|
|
params[:account][:turnitin_host] = validated_turnitin_host(params[:account][:turnitin_host])
|
|
enable_user_notes = params[:account].delete :enable_user_notes
|
|
allow_sis_import = params[:account].delete :allow_sis_import
|
|
params[:account].delete :default_user_storage_quota_mb unless @account.root_account? && !@account.site_admin?
|
|
unless @account.grants_right? @current_user, :manage_storage_quotas
|
|
[:storage_quota, :default_storage_quota, :default_storage_quota_mb,
|
|
:default_user_storage_quota, :default_user_storage_quota_mb,
|
|
:default_group_storage_quota, :default_group_storage_quota_mb].each { |key| params[:account].delete key }
|
|
end
|
|
if params[:account][:services]
|
|
params[:account][:services].slice(*Account.services_exposed_to_ui_hash(nil, @current_user, @account).keys).each do |key, value|
|
|
@account.set_service_availability(key, value == '1')
|
|
end
|
|
params[:account].delete :services
|
|
end
|
|
if @account.grants_right?(@current_user, :manage_site_settings)
|
|
# If the setting is present (update is called from 2 different settings forms, one for notifications)
|
|
if params[:account][:settings] && params[:account][:settings][:outgoing_email_default_name_option].present?
|
|
# If set to default, remove the custom name so it doesn't get saved
|
|
params[:account][:settings][:outgoing_email_default_name] = '' if params[:account][:settings][:outgoing_email_default_name_option] == 'default'
|
|
end
|
|
|
|
google_docs_domain = params[:account][:settings].try(:delete, :google_docs_domain)
|
|
if @account.feature_enabled?(:google_docs_domain_restriction) &&
|
|
@account.root_account? &&
|
|
!@account.site_admin?
|
|
@account.settings[:google_docs_domain] = google_docs_domain.present? ? google_docs_domain : nil
|
|
end
|
|
|
|
@account.enable_user_notes = enable_user_notes if enable_user_notes
|
|
@account.allow_sis_import = allow_sis_import if allow_sis_import && @account.root_account?
|
|
if @account.site_admin? && params[:account][:settings]
|
|
# these shouldn't get set for the site admin account
|
|
params[:account][:settings].delete(:enable_alerts)
|
|
params[:account][:settings].delete(:enable_eportfolios)
|
|
end
|
|
else
|
|
# must have :manage_site_settings to update these
|
|
[ :admins_can_change_passwords,
|
|
:admins_can_view_notifications,
|
|
:enable_alerts,
|
|
:enable_eportfolios,
|
|
:enable_profiles,
|
|
:show_scheduler,
|
|
:global_includes,
|
|
:gmail_domain
|
|
].each do |key|
|
|
params[:account][:settings].try(:delete, key)
|
|
end
|
|
end
|
|
|
|
if params[:account][:settings] && params[:account][:settings].has_key?(:trusted_referers)
|
|
if trusted_referers = params[:account][:settings].delete(:trusted_referers)
|
|
@account.trusted_referers = trusted_referers if @account.root_account?
|
|
end
|
|
end
|
|
|
|
if sis_id = params[:account].delete(:sis_source_id)
|
|
if !@account.root_account? && sis_id != @account.sis_source_id && @account.root_account.grants_right?(@current_user, session, :manage_sis)
|
|
if sis_id == ''
|
|
@account.sis_source_id = nil
|
|
else
|
|
@account.sis_source_id = sis_id
|
|
end
|
|
end
|
|
end
|
|
|
|
process_external_integration_keys
|
|
|
|
can_edit_email = params[:account][:settings].try(:delete, :edit_institution_email)
|
|
if @account.root_account? && !can_edit_email.nil?
|
|
@account[:settings][:edit_institution_email] = value_to_boolean(can_edit_email)
|
|
end
|
|
|
|
if @account.update_attributes(params[:account])
|
|
format.html { redirect_to account_settings_url(@account) }
|
|
format.json { render :json => @account }
|
|
else
|
|
flash[:error] = t(:update_failed_notice, "Account settings update failed")
|
|
format.html { redirect_to account_settings_url(@account) }
|
|
format.json { render :json => @account.errors, :status => :bad_request }
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def settings
|
|
if authorized_action(@account, @current_user, :read)
|
|
@available_reports = AccountReport.available_reports if @account.grants_right?(@current_user, @session, :read_reports)
|
|
if @available_reports
|
|
@last_complete_reports = {}
|
|
@last_reports = {}
|
|
if AccountReport.connection.adapter_name == 'PostgreSQL'
|
|
scope = @account.account_reports.select("DISTINCT ON (report_type) account_reports.*").order(:report_type)
|
|
@last_complete_reports = scope.last_complete_of_type(@available_reports.keys, nil).preload(:attachment).index_by(&:report_type)
|
|
@last_reports = scope.last_of_type(@available_reports.keys, nil).index_by(&:report_type)
|
|
else
|
|
@available_reports.keys.each do |report|
|
|
@last_complete_reports[report] = @account.account_reports.last_complete_of_type(report).first
|
|
@last_reports[report] = @account.account_reports.last_of_type(report).first
|
|
end
|
|
end
|
|
end
|
|
load_course_right_side
|
|
@account_users = @account.account_users
|
|
ActiveRecord::Associations::Preloader.new(@account_users, user: :communication_channels).run
|
|
order_hash = {}
|
|
@account.available_account_roles.each_with_index do |role, idx|
|
|
order_hash[role.id] = idx
|
|
end
|
|
@account_users = @account_users.select(&:user).sort_by{|au| [order_hash[au.role_id] || CanvasSort::Last, Canvas::ICU.collation_key(au.user.sortable_name)] }
|
|
@alerts = @account.alerts
|
|
|
|
@account_roles = @account.available_account_roles.sort_by(&:display_sort_index).map{|role| {:id => role.id, :label => role.label}}
|
|
@course_roles = @account.available_course_roles.sort_by(&:display_sort_index).map{|role| {:id => role.id, :label => role.label}}
|
|
|
|
@announcements = @account.announcements
|
|
@external_integration_keys = ExternalIntegrationKey.indexed_keys_for(@account)
|
|
|
|
js_env({
|
|
APP_CENTER: { enabled: Canvas::Plugin.find(:app_center).enabled? },
|
|
ENABLE_LTI2: @account.root_account.feature_enabled?(:lti2_ui),
|
|
LTI_LAUNCH_URL: account_tool_proxy_registration_path(@account),
|
|
CONTEXT_BASE_URL: "/accounts/#{@context.id}"
|
|
})
|
|
end
|
|
end
|
|
|
|
# Admin Tools page controls
|
|
# => Log Auditing
|
|
# => Add/Change Quota
|
|
# = Restoring Content
|
|
def admin_tools
|
|
if !@account.can_see_admin_tools_tab?(@current_user)
|
|
return render_unauthorized_action
|
|
end
|
|
|
|
authentication_logging = @account.grants_any_right?(@current_user, :view_statistics, :manage_user_logins)
|
|
grade_change_logging = @account.grants_right?(@current_user, :view_grade_changes)
|
|
course_logging = @account.grants_right?(@current_user, :view_course_changes)
|
|
if authentication_logging || grade_change_logging || course_logging
|
|
logging = {
|
|
authentication: authentication_logging,
|
|
grade_change: grade_change_logging,
|
|
course: course_logging
|
|
}
|
|
end
|
|
logging ||= false
|
|
|
|
js_env :ACCOUNT_ID => @account.id
|
|
js_env :PERMISSIONS => {
|
|
restore_course: @account.grants_right?(@current_user, session, :undelete_courses),
|
|
# Permission caching issue makes explicitly checking the account setting
|
|
# an easier option.
|
|
view_messages: (@account.settings[:admins_can_view_notifications] &&
|
|
@account.grants_right?(@current_user, session, :view_notifications)) ||
|
|
Account.site_admin.grants_right?(@current_user, :read_messages),
|
|
logging: logging
|
|
}
|
|
end
|
|
|
|
def confirm_delete_user
|
|
raise ActiveRecord::RecordNotFound unless @account.root_account?
|
|
@user = api_find(User, params[:user_id])
|
|
|
|
unless @account.user_account_associations.where(user_id: @user).exists?
|
|
flash[:error] = t(:no_user_message, "No user found with that id")
|
|
redirect_to account_url(@account)
|
|
return
|
|
end
|
|
|
|
@context = @account
|
|
render_unauthorized_action unless @user.allows_user_to_remove_from_account?(@account, @current_user)
|
|
end
|
|
|
|
# @API Delete a user from the root account
|
|
#
|
|
# Delete a user record from a Canvas root account. If a user is associated
|
|
# with multiple root accounts (in a multi-tenant instance of Canvas), this
|
|
# action will NOT remove them from the other accounts.
|
|
#
|
|
# WARNING: This API will allow a user to remove themselves from the account.
|
|
# If they do this, they won't be able to make API calls or log into Canvas at
|
|
# that account.
|
|
#
|
|
# @example_request
|
|
# curl https://<canvas>/api/v1/accounts/3/users/5 \
|
|
# -H 'Authorization: Bearer <ACCESS_TOKEN>' \
|
|
# -X DELETE
|
|
#
|
|
# @returns User
|
|
def remove_user
|
|
raise ActiveRecord::RecordNotFound unless @account.root_account?
|
|
@user = api_find(User, params[:user_id])
|
|
raise ActiveRecord::RecordNotFound unless @account.user_account_associations.where(user_id: @user).exists?
|
|
if @user.allows_user_to_remove_from_account?(@account, @current_user)
|
|
@user.remove_from_root_account(@account)
|
|
flash[:notice] = t(:user_deleted_message, "%{username} successfully deleted", :username => @user.name)
|
|
respond_to do |format|
|
|
format.html { redirect_to account_users_url(@account) }
|
|
format.json { render :json => @user || {} }
|
|
end
|
|
else
|
|
render_unauthorized_action
|
|
end
|
|
end
|
|
|
|
def turnitin_confirmation
|
|
if authorized_action(@account, @current_user, :manage_account_settings)
|
|
host = validated_turnitin_host(params[:turnitin_host])
|
|
begin
|
|
turnitin = Turnitin::Client.new(
|
|
params[:turnitin_account_id],
|
|
params[:turnitin_shared_secret],
|
|
host
|
|
)
|
|
render :json => { :success => turnitin.testSettings }
|
|
rescue
|
|
render :json => { :success => false }
|
|
end
|
|
end
|
|
end
|
|
|
|
def load_course_right_side
|
|
@root_account = @account.root_account
|
|
@maximum_courses_im_gonna_show = 50
|
|
@term = nil
|
|
if params[:enrollment_term_id].present?
|
|
@term = @root_account.enrollment_terms.active.find(params[:enrollment_term_id]) rescue nil
|
|
@term ||= @root_account.enrollment_terms.active[-1]
|
|
end
|
|
associated_courses = @account.associated_courses.active
|
|
associated_courses = associated_courses.for_term(@term) if @term
|
|
@associated_courses_count = associated_courses.count
|
|
@hide_enrollmentless_courses = params[:hide_enrollmentless_courses] == "1"
|
|
end
|
|
protected :load_course_right_side
|
|
|
|
def statistics
|
|
if authorized_action(@account, @current_user, :view_statistics)
|
|
add_crumb(t(:crumb_statistics, "Statistics"), statistics_account_url(@account))
|
|
if @account.grants_right?(@current_user, :read_course_list)
|
|
@recently_started_courses = @account.all_courses.recently_started
|
|
@recently_ended_courses = @account.all_courses.recently_ended
|
|
if @account == Account.default
|
|
@recently_created_courses = @account.all_courses.recently_created
|
|
end
|
|
end
|
|
if @account.grants_right?(@current_user, :read_roster)
|
|
@recently_logged_users = @account.all_users.recently_logged_in
|
|
end
|
|
@counts_report = @account.report_snapshots.detailed.order(:created_at).last.try(:data)
|
|
end
|
|
end
|
|
|
|
def statistics_graph
|
|
if authorized_action(@account, @current_user, :view_statistics)
|
|
@items = @account.report_snapshots.progressive.last.try(:report_value_over_time, params[:attribute])
|
|
respond_to do |format|
|
|
format.json { render :json => @items }
|
|
format.csv {
|
|
res = CSV.generate do |csv|
|
|
csv << ['Timestamp', 'Value']
|
|
@items.each do |item|
|
|
csv << [item[0]/1000, item[1]]
|
|
end
|
|
end
|
|
cancel_cache_buster
|
|
# TODO i18n
|
|
send_data(
|
|
res,
|
|
:type => "text/csv",
|
|
:filename => "#{params[:attribute].titleize} Report for #{@account.name}.csv",
|
|
:disposition => "attachment"
|
|
)
|
|
}
|
|
end
|
|
end
|
|
end
|
|
|
|
def avatars
|
|
if authorized_action(@account, @current_user, :manage_admin_users)
|
|
@users = @account.all_users(nil)
|
|
@avatar_counts = {
|
|
:all => format_avatar_count(@users.with_avatar_state('any').count),
|
|
:reported => format_avatar_count(@users.with_avatar_state('reported').count),
|
|
:re_reported => format_avatar_count(@users.with_avatar_state('re_reported').count),
|
|
:submitted => format_avatar_count(@users.with_avatar_state('submitted').count)
|
|
}
|
|
if params[:avatar_state]
|
|
@users = @users.with_avatar_state(params[:avatar_state])
|
|
@avatar_state = params[:avatar_state]
|
|
else
|
|
if @domain_root_account && @domain_root_account.settings[:avatars] == 'enabled_pending'
|
|
@users = @users.with_avatar_state('submitted')
|
|
@avatar_state = 'submitted'
|
|
else
|
|
@users = @users.with_avatar_state('reported')
|
|
@avatar_state = 'reported'
|
|
end
|
|
end
|
|
@users = Api.paginate(@users, self, account_avatars_url)
|
|
end
|
|
end
|
|
|
|
def sis_import
|
|
if authorized_action(@account, @current_user, :manage_sis)
|
|
return redirect_to account_settings_url(@account) if !@account.allow_sis_import || !@account.root_account?
|
|
@current_batch = @account.current_sis_batch
|
|
@last_batch = @account.sis_batches.order('created_at DESC').first
|
|
@terms = @account.enrollment_terms.active
|
|
respond_to do |format|
|
|
format.html
|
|
format.json { render :json => @current_batch }
|
|
end
|
|
end
|
|
end
|
|
|
|
def courses_redirect
|
|
redirect_to course_url(params[:id])
|
|
end
|
|
|
|
def courses
|
|
if authorized_action(@context, @current_user, :read)
|
|
Shackles.activate(:slave) do
|
|
load_course_right_side
|
|
@courses = []
|
|
@query = (params[:course] && params[:course][:name]) || params[:term]
|
|
if @context && @context.is_a?(Account) && @query
|
|
@courses = @context.courses_name_like(@query, :term => @term, :hide_enrollmentless_courses => @hide_enrollmentless_courses)
|
|
end
|
|
end
|
|
respond_to do |format|
|
|
format.html {
|
|
return redirect_to @courses.first if @courses.length == 1
|
|
Shackles.activate(:slave) do
|
|
build_course_stats
|
|
end
|
|
}
|
|
format.json {
|
|
cancel_cache_buster
|
|
expires_in 30.minutes
|
|
render :json => @courses.map{ |c| {:label => c.name, :id => c.id, :term => c.enrollment_term.name} }
|
|
}
|
|
end
|
|
end
|
|
end
|
|
|
|
def build_course_stats
|
|
teachers = TeacherEnrollment.for_courses_with_user_name(@courses).admin.active
|
|
course_to_student_counts = StudentEnrollment.student_in_claimed_or_available.where(:course_id => @courses).group(:course_id).count(:user_id, :distinct => true)
|
|
courses_to_teachers = teachers.inject({}) do |result, teacher|
|
|
result[teacher.course_id] ||= []
|
|
result[teacher.course_id] << teacher
|
|
result
|
|
end
|
|
@courses.each do |course|
|
|
course.student_count = course_to_student_counts[course.id] || 0
|
|
course_teachers = courses_to_teachers[course.id] || []
|
|
course.teacher_names = course_teachers.uniq(&:user_id).map(&:user_name)
|
|
end
|
|
end
|
|
protected :build_course_stats
|
|
|
|
def saml_meta_data
|
|
# This needs to be publicly available since external SAML
|
|
# servers need to be able to access it without being authenticated.
|
|
# It is used to disclose our SAML configuration settings.
|
|
settings = AccountAuthorizationConfig::SAML.saml_settings_for_account(@domain_root_account, request.host_with_port)
|
|
render :xml => Onelogin::Saml::MetaData.create(settings)
|
|
end
|
|
|
|
# TODO Refactor add_account_user and remove_account_user actions into
|
|
# AdminsController. see https://redmine.instructure.com/issues/6634
|
|
def add_account_user
|
|
if role_id = params[:role_id]
|
|
role = Role.get_role_by_id(role_id)
|
|
raise ActiveRecord::RecordNotFound unless role
|
|
else
|
|
role = Role.get_built_in_role('AccountAdmin')
|
|
end
|
|
|
|
list = UserList.new(params[:user_list],
|
|
:root_account => @context.root_account,
|
|
:search_method => @context.user_list_search_mode_for(@current_user))
|
|
users = list.users
|
|
admins = users.map do |user|
|
|
admin = @context.account_users.where(user_id: user.id, role_id: role.id).first_or_initialize
|
|
admin.user = user
|
|
return unless authorized_action(admin, @current_user, :create)
|
|
admin
|
|
end
|
|
|
|
account_users = admins.map do |admin|
|
|
if admin.new_record?
|
|
admin.save!
|
|
if admin.user.registered?
|
|
admin.account_user_notification!
|
|
else
|
|
admin.account_user_registration!
|
|
end
|
|
end
|
|
|
|
{ :enrollment => {
|
|
:id => admin.id,
|
|
:name => admin.user.name,
|
|
:role_id => admin.role_id,
|
|
:membership_type => AccountUser.readable_type(admin.role.name),
|
|
:workflow_state => 'active',
|
|
:user_id => admin.user.id,
|
|
:type => 'admin',
|
|
:email => admin.user.email
|
|
}}
|
|
end
|
|
render :json => account_users
|
|
end
|
|
|
|
def remove_account_user
|
|
admin = @context.account_users.find(params[:id])
|
|
if authorized_action(admin, @current_user, :destroy)
|
|
admin.destroy
|
|
respond_to do |format|
|
|
format.html { redirect_to account_settings_url(@context, :anchor => "tab-users") }
|
|
format.json { render :json => admin }
|
|
end
|
|
end
|
|
end
|
|
|
|
def validated_turnitin_host(input_host)
|
|
if input_host.present?
|
|
_, turnitin_uri = CanvasHttp.validate_url(input_host)
|
|
turnitin_uri.host
|
|
else
|
|
nil
|
|
end
|
|
end
|
|
|
|
def process_external_integration_keys
|
|
if params_keys = params[:account][:external_integration_keys]
|
|
ExternalIntegrationKey.indexed_keys_for(@account).each do |key_type, key|
|
|
next unless params_keys.key?(key_type)
|
|
next unless key.grants_right?(@current_user, :write)
|
|
unless params_keys[key_type].blank?
|
|
key.key_value = params_keys[key_type]
|
|
key.save!
|
|
else
|
|
key.delete
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def format_avatar_count(count = 0)
|
|
count > 99 ? "99+" : count
|
|
end
|
|
private :format_avatar_count
|
|
|
|
end
|