canvas-lms/app/models/account.rb

1210 lines
47 KiB
Ruby
Raw Normal View History

2011-02-01 09:57:29 +08:00
#
# Copyright (C) 2011 - 2012 Instructure, Inc.
2011-02-01 09:57:29 +08:00
#
# 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/>.
#
class Account < ActiveRecord::Base
include Context
attr_accessible :name, :turnitin_account_id,
2011-02-01 09:57:29 +08:00
:turnitin_shared_secret, :turnitin_comments, :turnitin_pledge,
:default_time_zone, :parent_account, :settings, :default_storage_quota,
:default_storage_quota_mb, :storage_quota, :ip_filters, :default_locale,
:default_user_storage_quota_mb
2011-02-01 09:57:29 +08:00
include Workflow
belongs_to :parent_account, :class_name => 'Account'
belongs_to :root_account, :class_name => 'Account'
authenticates_many :pseudonym_sessions
has_many :courses
has_many :all_courses, :class_name => 'Course', :foreign_key => 'root_account_id'
has_many :group_categories, :as => :context, :conditions => ['deleted_at IS NULL']
has_many :all_group_categories, :class_name => 'GroupCategory', :as => :context
2011-02-01 09:57:29 +08:00
has_many :groups, :as => :context
has_many :all_groups, :class_name => 'Group', :foreign_key => 'root_account_id'
2011-02-01 09:57:29 +08:00
has_many :enrollment_terms, :foreign_key => 'root_account_id'
student view; closes #6995 allows course admins to view the course from a student perspective. this is accessible from a button on the course/settings page. They should be able to interact with the course as a student would, including submitting homework and quizzes. Right now there is one student view student per course, so if the course has multiple administrators, they will all share the same student view student. There are a few things that won't work in student view the way the would for a normal student, most notably access to conversations is disabled. Additionally, any publicly visible action that the teacher takes while in student view will still be publicly visible -- for example if the teacher posts a discussion topic/reply as the student view student, it will be visible to the whole class. test-plan: - (the following should be tried both as a full teacher and as a section-limited course admin) - set up a few assignments, quizzes, discussions, and module progressions in a course. - enter student view from the coures settings page. - work through the things you set up above. - leave student view from the upper right corner of the page. - as a teacher you should be able to grade the fake student so that they can continue to progress. - the student should not show up in the course users list - the student should not show up at the account level at all: * total user list * statistics Change-Id: I886a4663777f3ef2bdae594349ff6da6981e14ed Reviewed-on: https://gerrit.instructure.com/9484 Tested-by: Hudson <hudson@instructure.com> Reviewed-by: Cody Cutrer <cody@instructure.com>
2012-03-14 04:08:19 +08:00
has_many :enrollments, :foreign_key => 'root_account_id', :conditions => ["enrollments.type != 'StudentViewEnrollment'"]
2011-02-01 09:57:29 +08:00
has_many :sub_accounts, :class_name => 'Account', :foreign_key => 'parent_account_id', :conditions => ['workflow_state != ?', 'deleted']
has_many :all_accounts, :class_name => 'Account', :foreign_key => 'root_account_id', :order => 'name'
2011-02-01 09:57:29 +08:00
has_many :account_users, :dependent => :destroy
has_many :course_sections, :foreign_key => 'root_account_id'
has_many :sis_batches
has_many :abstract_courses, :class_name => 'AbstractCourse', :foreign_key => 'account_id'
has_many :root_abstract_courses, :class_name => 'AbstractCourse', :foreign_key => 'root_account_id'
2011-02-01 09:57:29 +08:00
has_many :users, :through => :account_users
has_many :pseudonyms, :include => :user
has_many :role_overrides, :as => :context
has_many :rubrics, :as => :context
has_many :rubric_associations, :as => :context, :include => :rubric, :dependent => :destroy
has_many :course_account_associations
has_many :associated_courses, :through => :course_account_associations, :source => :course, :select => 'DISTINCT courses.*'
2011-02-01 09:57:29 +08:00
has_many :child_courses, :through => :course_account_associations, :source => :course, :conditions => ['course_account_associations.depth = 0']
has_many :attachments, :as => :context, :dependent => :destroy
has_many :active_assignments, :as => :context, :class_name => 'Assignment', :conditions => ['assignments.workflow_state != ?', 'deleted']
has_many :folders, :as => :context, :dependent => :destroy, :order => 'folders.name'
has_many :active_folders, :class_name => 'Folder', :as => :context, :conditions => ['folders.workflow_state != ?', 'deleted'], :order => 'folders.name'
has_many :active_folders_with_sub_folders, :class_name => 'Folder', :as => :context, :include => [:active_sub_folders], :conditions => ['folders.workflow_state != ?', 'deleted'], :order => 'folders.name'
2011-02-01 09:57:29 +08:00
has_many :active_folders_detailed, :class_name => 'Folder', :as => :context, :include => [:active_sub_folders, :active_file_attachments], :conditions => ['folders.workflow_state != ?', 'deleted'], :order => 'folders.name'
has_many :account_authorization_configs, :order => "position"
2011-02-01 09:57:29 +08:00
has_many :account_reports
has_many :grading_standards, :as => :context, :conditions => ['workflow_state != ?', 'deleted']
has_many :assessment_questions, :through => :assessment_question_banks
has_many :assessment_question_banks, :as => :context, :include => [:assessment_questions, :assessment_question_bank_users]
has_many :roles
has_many :all_roles, :class_name => 'Role', :foreign_key => 'root_account_id'
def inherited_assessment_question_banks(include_self = false, *additional_contexts)
sql = []
conds = []
contexts = additional_contexts + account_chain
contexts.delete(self) unless include_self
contexts.each { |c|
sql << "context_type = ? AND context_id = ?"
conds += [c.class.to_s, c.id]
}
conds.unshift(sql.join(" OR "))
AssessmentQuestionBank.scoped :conditions => conds
end
2011-02-01 09:57:29 +08:00
learning outcomes refactor This list is *NOT* complete, some items may have snuck in that I forgot to note, and/or some of the noted items may not be completely functional yet. Specs need to be written around a lot of this, other specs will no doubt need to be fixed. Some things, particularly around LearningOutcomeGroups will need data migrations that aren't there yet. * remove LearningOutcome.non_rubric_outcomes? and replace with false where invoked * remove LearningOutcome.enabled? and replace with true where invoked * remove never-taken branches * remove the shared/aligned_outcomes partial and it's supporting javascript, since it's now empty * remove js handler for add_outcome_alignment_link and supporting method since it only occurred in never-taken branches * mix LearningOutcomeContext into Course and Account * replace LearningOutcomeGroup.default_for(context) with LearningOutcomeContext#root_outcome_group * rename LearningOutcome#content_tags to LearningOutcome#alignments * rename LearningOutcomeGroup#content_tags to LearningOutcomeGroup#child_links, and properly restrict * remove ContentTag[Alignment]#rubric_association_id, add ContentTag[Alignment]#has_rubric_association? that looks at the presence of the content's rubric_association_id * condition off the assignment having a rubric_association rather than filtering tags by has_rubric_association (which just looks back at the assignment). all or none of the assignment's alignments are forced to have the association (via the assignment). this was true in practice before, is now codified (and more efficient) * rename AssessmentQuestionBank#learning_outcome_tags to AssessmentQuestionBank#learning_outcome_alignments * rename Assignment#learning_outcome_tags to Assignment#learning_outcome_alignments * rename Rubric#learning_outcome_tags to Rubric#learning_outcome_alignments * move/rename (Course|Account)#learning_outcome_tags to LearningOutcomeContext#learning_outcome_links * move/rename Account#learning_outcomes (corrected) and Course#learning_outcomes to LearningOutcomeContext#linked_learning_outcomes * move/rename Account#created_learning_outcomes and Course#created_learning_outcomes to LearningOutcomeContext#created_learning_outcomes * clarify and correct usage of linked_learning_outcomes vs. created_learning_outcomes * move/rename (Account|Account)#learning_outcome_groups to LearningOutcomeContext#learning_outcome_groups * remove unused Account#associated_learning_outcomes * just remove one link to a learning outcome when deleting * merge Account#has_outcomes?, Course#has_outcomes? and Course#has_outcomes into LearningOutcomeContext#has_outcomes?, add a use in Context#active_record_types * kill LearningOutcomeGroup#root_learning_outcome_group (unused) * rename LearningOutcomeResult#content_tag to LearningOutcomeResult#alignment * kill unused (and broken) OutcomesController#add_outcome_group * kill unused OutcomesController#update_outcomes_for_asset * kill unused OutcomesController#outcomes_for_asset * remove unused (outside specs, correct specs) AssessmentQuestionBank#outcomes= * remove unused ContentTag#learning_outcome_content * replace ContentTag.learning_outcome_tags_for(asset) (only ever called with asset=an assignment) with call to Assignment#learning_outcome_alignments * remove unused ContentTag.not_rubric * remove (now) unused ContentTag.include_outcome * remove unused LearningOutcome#learning_outcome_group_associations * avoid explicit use of ContentTag in outcome-related specs * replace LearningOutcomeGroup#learning_outcome_tags with LearningOutcomeGroup#child_outcome_links (and only use for outcome links; not tags for child groups) * split ContentTag#create_outcome_result into Submission#create_outcome_result, QuizSubmission#create_outcome_result, and RubricAssessment#create_outcome_result. fix some bugs along the way * refactor ContentTag.outcome_tags_for_banks and some code from QuizSubmission#(track_outcomes|update_outcomes_for_assessment_questions) into QuizSubmission#questions_and_alignments * refactor RubricAssociation#update_outcome_relations and Rubric#update_alignments into LearningOutcome.update_alignments * don't use ContentTag#rubric_association with outcome alignments; use the tag's content's rubric_association in its place (they should have been equal anyways) * refactor LearningOutcome.available_in_context and @context.root_outcome_group.sorted_all_outcomes (only time sorted_all_outcomes is used) into LearningOutcomeContext#available_outcomes and LearningOutcomeContext#available_outcome * overhaul LearningOutcomeGroup#sorted_content and rename to LearningOutcomeGroup#sorted_children. it not returns ContentTags (outcome links) and LearningOutcomeGroups, vs. LearningOutcomes and LearningOutcomeGroups; fix usages appropriately * fix UI for arranging/deleting outcome links and groups within a group to refer to the outcome link rather than the outcome Change-Id: I85d99f2634f7206332cb1f5d5ea575b428988d4b Reviewed-on: https://gerrit.instructure.com/12590 Reviewed-by: Jacob Fugal <jacob@instructure.com> Tested-by: Jacob Fugal <jacob@instructure.com>
2012-07-13 01:16:13 +08:00
include LearningOutcomeContext
has_many :context_external_tools, :as => :context, :dependent => :destroy, :order => 'name'
2011-02-01 09:57:29 +08:00
has_many :error_reports
has_many :announcements, :class_name => 'AccountNotification'
has_many :alerts, :as => :context, :include => :criteria
has_many :associated_alerts, :through => :associated_courses, :source => :alerts, :include => :criteria
has_many :user_account_associations
has_many :report_snapshots
2011-02-01 09:57:29 +08:00
before_validation :verify_unique_sis_source_id
2011-02-01 09:57:29 +08:00
before_save :ensure_defaults
before_save :set_update_account_associations_if_changed
after_save :update_account_associations_if_changed
after_create :default_enrollment_term
2011-02-01 09:57:29 +08:00
serialize :settings, Hash
scopes_custom_fields
validates_locale :default_locale, :allow_nil => true
validate :account_chain_loop, :if => :parent_account_id_changed?
validate :validate_auth_discovery_url
include StickySisFields
are_sis_sticky :name
def default_locale(recurse = false)
read_attribute(:default_locale) ||
(recurse && parent_account ? parent_account.default_locale(true) : nil)
end
2011-02-01 09:57:29 +08:00
cattr_accessor :account_settings_options
self.account_settings_options = {}
# I figure we're probably going to be adding more account-level
# settings in the future (and moving some of the column attributes
# to the settings hash), so it makes sense to have a general way
# of defining what settings are allowed when. Somebody please tell
# me if I'm overarchitecting...
def self.add_setting(setting, opts=nil)
self.account_settings_options[setting.to_sym] = opts || {}
refactor user creation/invitations closes #5833 fixes #5573, #5572, #5753 * communication channels are now only unique within a single user * UserList changes * Always resolve pseudonym#unique_ids * Support looking up by SMS CCs * Option to either require e-mails match an existing CC, or e-mails that don't match a Pseudonym will always be returned unattached (relying on better merging behavior to not have a gazillion accounts created) * Method to return users, creating new ones (*without* a Pseudonym) if necessary. (can't create with a pseudonym, since Pseudonym#unique_id is still unique, I can't have multiple outstanding users with the same unique_id) * EnrollmentsFromUserList is mostly gutted, now using UserList's functionality directy. * Use UserList for adding account admins, removing the now unused Account#add_admin => User#find_by_email/User#assert_by_email codepath * Update UsersController#create to not worry about duplicate communication channels * Remove AccountsController#add_user, and just use UsersController#create * Change SIS::UserImporter to send out a merge opportunity e-mail if a conflicting CC is found (but still create the CC) * In /profile, don't worry about conflicting CCs (the CC confirmation process will now allow merging) * Remove CommunicationChannelsController#try_merge and #merge * For the non-simple case of CoursesController#enrollment_invitation redirect to /register (CommunicationsChannelController#confirm) * Remove CoursesController#transfer_enrollment * Move PseudonymsController#registration_confirmation to CommunicationChannelsController#confirm (have to be able to register an account without a Pseudonym yet) * Fold the old direct confirm functionality in, if there are no available merge opportunities * Allow merging the new account with the currently logged in user * Allow changing the Pseudonym#unique_id when registering a new account (since there might be conflicts) * Display a list of merge opportunities based on conflicting communication channels * Provide link(s) to log in as the other user, redirecting back to the registration page after login is complete (to complete the merge as the current user) * Remove several assert_* methods that are no longer needed * Update PseudonymSessionsController a bit to deal with the new way of dealing with conflicting CCs (especially CCs from LDAP), and to redirect back to the registration/confirmation page when attempting to do a merge * Expose the open_registration setting; use it to control if inviting users to a course is able to create new users Change-Id: If2f38818a71af656854d3bf8431ddbf5dcb84691 Reviewed-on: https://gerrit.instructure.com/6149 Tested-by: Hudson <hudson@instructure.com> Reviewed-by: Jacob Fugal <jacob@instructure.com>
2011-10-13 04:30:48 +08:00
if (opts && opts[:boolean] && opts.has_key?(:default))
if opts[:default]
self.class_eval "def #{setting}?; settings[:#{setting}] != false; end"
else
self.class_eval "def #{setting}?; !!settings[:#{setting}]; end"
end
end
2011-02-01 09:57:29 +08:00
end
# these settings either are or could be easily added to
# the account settings page
add_setting :global_includes, :root_only => true, :boolean => true, :default => false
sub-account branding; closes #9368 allow sub accounts to include their own global scripts and stylesheets. if global includes are enabled on the root account, root account administrators will have an option to enable them for immediate child accounts. those child accounts can then choose to enable them for their sub-accounts, and so on down the chain. these includes are added to the page in order from highest to lowest account, so sub-accounts are able to override styles added by their parents. the logic for which styles to display on which pages is as follows: - on account pages, include all styles in the chain from this account up to the root account. this ensures that you can always see styles for account X without any sub-account overrides on account X's page - on course/group pages, include all styles in the chain from the account which contains that course/group up to the root - on the dashboard, calendar, user pages, and other pages that don't fall into one of the above categories, we find the lowest account that contains all of the current user's active classes + groups, and include styles from that account up to the root test plan: - in a root account, create two sub-accounts, create courses in each of them, and create 3 users, one enrolled only in the first course, one only in the second course, and one enrolled in both courses. - enable global includes on the root account (no sub-accounts yet) add files, and make sure all three students see them. - now enable sub-account includes, and add include files to each sub-account - make sure both users in course 1 see include for sub-account 1 - make sure user 1 sees include for sub-account 1 on her dashboard, but user 3 does not. Change-Id: I3d07d4bced39593f3084d5eac6ea3137666e319b Reviewed-on: https://gerrit.instructure.com/12248 Tested-by: Jenkins <jenkins@instructure.com> Reviewed-by: Simon Williams <simon@instructure.com>
2012-07-10 05:30:16 +08:00
add_setting :global_javascript, :condition => :allow_global_includes
add_setting :global_stylesheet, :condition => :allow_global_includes
add_setting :sub_account_includes, :condition => :allow_global_includes, :boolean => true, :default => false
2011-02-01 09:57:29 +08:00
add_setting :error_reporting, :hash => true, :values => [:action, :email, :url, :subject_param, :body_param], :root_only => true
add_setting :custom_help_links, :root_only => true
2011-02-01 09:57:29 +08:00
add_setting :prevent_course_renaming_by_teachers, :boolean => true, :root_only => true
refactor user creation/invitations closes #5833 fixes #5573, #5572, #5753 * communication channels are now only unique within a single user * UserList changes * Always resolve pseudonym#unique_ids * Support looking up by SMS CCs * Option to either require e-mails match an existing CC, or e-mails that don't match a Pseudonym will always be returned unattached (relying on better merging behavior to not have a gazillion accounts created) * Method to return users, creating new ones (*without* a Pseudonym) if necessary. (can't create with a pseudonym, since Pseudonym#unique_id is still unique, I can't have multiple outstanding users with the same unique_id) * EnrollmentsFromUserList is mostly gutted, now using UserList's functionality directy. * Use UserList for adding account admins, removing the now unused Account#add_admin => User#find_by_email/User#assert_by_email codepath * Update UsersController#create to not worry about duplicate communication channels * Remove AccountsController#add_user, and just use UsersController#create * Change SIS::UserImporter to send out a merge opportunity e-mail if a conflicting CC is found (but still create the CC) * In /profile, don't worry about conflicting CCs (the CC confirmation process will now allow merging) * Remove CommunicationChannelsController#try_merge and #merge * For the non-simple case of CoursesController#enrollment_invitation redirect to /register (CommunicationsChannelController#confirm) * Remove CoursesController#transfer_enrollment * Move PseudonymsController#registration_confirmation to CommunicationChannelsController#confirm (have to be able to register an account without a Pseudonym yet) * Fold the old direct confirm functionality in, if there are no available merge opportunities * Allow merging the new account with the currently logged in user * Allow changing the Pseudonym#unique_id when registering a new account (since there might be conflicts) * Display a list of merge opportunities based on conflicting communication channels * Provide link(s) to log in as the other user, redirecting back to the registration page after login is complete (to complete the merge as the current user) * Remove several assert_* methods that are no longer needed * Update PseudonymSessionsController a bit to deal with the new way of dealing with conflicting CCs (especially CCs from LDAP), and to redirect back to the registration/confirmation page when attempting to do a merge * Expose the open_registration setting; use it to control if inviting users to a course is able to create new users Change-Id: If2f38818a71af656854d3bf8431ddbf5dcb84691 Reviewed-on: https://gerrit.instructure.com/6149 Tested-by: Hudson <hudson@instructure.com> Reviewed-by: Jacob Fugal <jacob@instructure.com>
2011-10-13 04:30:48 +08:00
add_setting :teachers_can_create_courses, :boolean => true, :root_only => true, :default => false
add_setting :students_can_create_courses, :boolean => true, :root_only => true, :default => false
add_setting :no_enrollments_can_create_courses, :boolean => true, :root_only => true, :default => false
add_setting :allow_sending_scores_in_emails, :boolean => true, :root_only => true
2011-02-01 09:57:29 +08:00
add_setting :support_url, :root_only => true
add_setting :self_enrollment
add_setting :equella_endpoint
add_setting :equella_teaser
add_setting :enable_alerts, :boolean => true, :root_only => true
add_setting :enable_eportfolios, :boolean => true, :root_only => true
add_setting :users_can_edit_name, :boolean => true, :root_only => true
add_setting :open_registration, :boolean => true, :root_only => true
add_setting :enable_scheduler, :boolean => true, :root_only => true, :default => false
add_setting :calendar2_only, :boolean => true, :root_only => true, :default => false
add_setting :show_scheduler, :boolean => true, :root_only => true, :default => false
add_setting :enable_profiles, :boolean => true, :root_only => true, :default => false
multi-factor authentication closes #9532 test plan: * enable optional MFA, and check the following: * normal log in should not be affected * you can enroll in MFA from your profile page * you can re-enroll in MFA from your profile page * you can disable MFA from your profile page * MFA can be reset by an admin on your user page * when enrolled, you are asked for verification code after username/password when logging in * you can't access any other part of the site directly until until entering your verification code * enable required MFA, and check the following * when not enrolled in MFA, and you log in, you are forced to enroll * you cannot disable MFA from your profile page * you can re-enroll in MFA from your profile page * an admin (other than himself) can reset MFA from the user page * for enrolling in MFA * use Google Authenticator and scan the QR code; you should have 30-seconds or so of extra leeway to enter your code * having no SMS communication channels on your profile, the enrollment page should just have a form to add a new phone * having one or more SMS communication channels on your profile, the enrollment page should list them, or allow you to create a new one (and switch back) * having more than one SMS communication channel on your profile, the enrollment page should remember which one you have selected after you click "send" * an unconfirmed SMS channel should go to confirmed when it's used to enroll in MFA * you should not be able to go directly to /login/otp to enroll if you used "Remember me" token to log in * MFA login flow * if configured with SMS, it should send you an SMS after you put in your username/password; you should have about 5 minutes of leeway to put it in * if you don't check "remember computer" checkbox, you should have to enter a verification code each time you log in * if you do check it, you shouldn't have to enter your code anymore (for three days). it also shouldn't SMS you a verification code each time you log in * setting MFA to required for admins should make it required for admins, optional for other users * with MFA enabled, directly go to /login/otp after entering username/password but before entering a verification code; it should send you back to the main login page * if you enrolled via SMS, you should not be able to remove that SMS from your profile * there should not be a reset MFA link on a user page if they haven't enrolled * test a login or required enrollment sequence with CAS and/or SAML Change-Id: I692de7405bf7ca023183e717930ee940ccf0d5e6 Reviewed-on: https://gerrit.instructure.com/12700 Tested-by: Jenkins <jenkins@instructure.com> Reviewed-by: Brian Palmer <brianp@instructure.com>
2012-08-03 05:17:50 +08:00
add_setting :mfa_settings, :root_only => true
add_setting :canvas_authentication, :boolean => true, :root_only => true
add_setting :admins_can_change_passwords, :boolean => true, :root_only => true, :default => false
add_setting :outgoing_email_default_name
add_setting :external_notification_warning, :boolean => true, :default => false
# When a user is invited to a course, do we let them see a preview of the
# course even without registering? This is part of the free-for-teacher
# account perks, since anyone can invite anyone to join any course, and it'd
# be nice to be able to see the course first if you weren't expecting the
# invitation.
add_setting :allow_invitation_previews, :boolean => true, :root_only => true, :default => false
2011-02-01 09:57:29 +08:00
def settings=(hash)
if hash.is_a?(Hash)
hash.each do |key, val|
if account_settings_options && account_settings_options[key.to_sym]
opts = account_settings_options[key.to_sym]
if (opts[:root_only] && !self.root_account?) || (opts[:condition] && !self.send("#{opts[:condition]}?".to_sym))
2011-02-01 09:57:29 +08:00
settings.delete key.to_sym
elsif opts[:boolean]
settings[key.to_sym] = (val == true || val == 'true' || val == '1' || val == 'on')
elsif opts[:hash]
new_hash = {}
if val.is_a?(Hash)
val.each do |inner_key, inner_val|
if opts[:values].include?(inner_key.to_sym)
new_hash[inner_key.to_sym] = inner_val.to_s
end
end
end
settings[key.to_sym] = new_hash.empty? ? nil : new_hash
else
settings[key.to_sym] = val.to_s
end
end
end
end
settings
end
sub-account branding; closes #9368 allow sub accounts to include their own global scripts and stylesheets. if global includes are enabled on the root account, root account administrators will have an option to enable them for immediate child accounts. those child accounts can then choose to enable them for their sub-accounts, and so on down the chain. these includes are added to the page in order from highest to lowest account, so sub-accounts are able to override styles added by their parents. the logic for which styles to display on which pages is as follows: - on account pages, include all styles in the chain from this account up to the root account. this ensures that you can always see styles for account X without any sub-account overrides on account X's page - on course/group pages, include all styles in the chain from the account which contains that course/group up to the root - on the dashboard, calendar, user pages, and other pages that don't fall into one of the above categories, we find the lowest account that contains all of the current user's active classes + groups, and include styles from that account up to the root test plan: - in a root account, create two sub-accounts, create courses in each of them, and create 3 users, one enrolled only in the first course, one only in the second course, and one enrolled in both courses. - enable global includes on the root account (no sub-accounts yet) add files, and make sure all three students see them. - now enable sub-account includes, and add include files to each sub-account - make sure both users in course 1 see include for sub-account 1 - make sure user 1 sees include for sub-account 1 on her dashboard, but user 3 does not. Change-Id: I3d07d4bced39593f3084d5eac6ea3137666e319b Reviewed-on: https://gerrit.instructure.com/12248 Tested-by: Jenkins <jenkins@instructure.com> Reviewed-by: Simon Williams <simon@instructure.com>
2012-07-10 05:30:16 +08:00
def allow_global_includes?
self.global_includes? || self.parent_account.try(:sub_account_includes?)
end
def global_includes_hash
includes = {}
if allow_global_includes?
includes = {}
includes[:js] = settings[:global_javascript] if settings[:global_javascript].present?
includes[:css] = settings[:global_stylesheet] if settings[:global_stylesheet].present?
end
includes.present? ? includes : nil
end
multi-factor authentication closes #9532 test plan: * enable optional MFA, and check the following: * normal log in should not be affected * you can enroll in MFA from your profile page * you can re-enroll in MFA from your profile page * you can disable MFA from your profile page * MFA can be reset by an admin on your user page * when enrolled, you are asked for verification code after username/password when logging in * you can't access any other part of the site directly until until entering your verification code * enable required MFA, and check the following * when not enrolled in MFA, and you log in, you are forced to enroll * you cannot disable MFA from your profile page * you can re-enroll in MFA from your profile page * an admin (other than himself) can reset MFA from the user page * for enrolling in MFA * use Google Authenticator and scan the QR code; you should have 30-seconds or so of extra leeway to enter your code * having no SMS communication channels on your profile, the enrollment page should just have a form to add a new phone * having one or more SMS communication channels on your profile, the enrollment page should list them, or allow you to create a new one (and switch back) * having more than one SMS communication channel on your profile, the enrollment page should remember which one you have selected after you click "send" * an unconfirmed SMS channel should go to confirmed when it's used to enroll in MFA * you should not be able to go directly to /login/otp to enroll if you used "Remember me" token to log in * MFA login flow * if configured with SMS, it should send you an SMS after you put in your username/password; you should have about 5 minutes of leeway to put it in * if you don't check "remember computer" checkbox, you should have to enter a verification code each time you log in * if you do check it, you shouldn't have to enter your code anymore (for three days). it also shouldn't SMS you a verification code each time you log in * setting MFA to required for admins should make it required for admins, optional for other users * with MFA enabled, directly go to /login/otp after entering username/password but before entering a verification code; it should send you back to the main login page * if you enrolled via SMS, you should not be able to remove that SMS from your profile * there should not be a reset MFA link on a user page if they haven't enrolled * test a login or required enrollment sequence with CAS and/or SAML Change-Id: I692de7405bf7ca023183e717930ee940ccf0d5e6 Reviewed-on: https://gerrit.instructure.com/12700 Tested-by: Jenkins <jenkins@instructure.com> Reviewed-by: Brian Palmer <brianp@instructure.com>
2012-08-03 05:17:50 +08:00
def mfa_settings
settings[:mfa_settings].try(:to_sym) || :disabled
end
def canvas_authentication?
settings[:canvas_authentication] != false || !self.account_authorization_config
end
def open_registration?
!!settings[:open_registration] && canvas_authentication?
end
def ip_filters=(params)
filters = {}
require 'ipaddr'
params.each do |key, str|
ips = []
vals = str.split(/,/)
vals.each do |val|
ip = IPAddr.new(val) rescue nil
# right now the ip_filter column on quizzes is just a string,
# so it has a max length. I figure whatever we set it to this
# setter should at the very least limit stored values to that
# length.
ips << val if ip && val.length <= 255
end
filters[key] = ips.join(',') unless ips.empty?
end
settings[:ip_filters] = filters
end
2011-02-01 09:57:29 +08:00
def ensure_defaults
self.uuid ||= AutoHandle.generate_securish_uuid
2011-02-01 09:57:29 +08:00
end
def verify_unique_sis_source_id
return true unless self.sis_source_id
if self.root_account?
self.errors.add(:sis_source_id, t('#account.root_account_cant_have_sis_id', "SIS IDs cannot be set on root accounts"))
return false
end
root = self.root_account
existing_account = Account.find_by_root_account_id_and_sis_source_id(root.id, self.sis_source_id)
return true if !existing_account || existing_account.id == self.id
self.errors.add(:sis_source_id, t('#account.sis_id_in_use', "SIS ID \"%{sis_id}\" is already in use", :sis_id => self.sis_source_id))
false
end
2011-02-01 09:57:29 +08:00
def set_update_account_associations_if_changed
self.root_account_id ||= self.parent_account.root_account_id if self.parent_account
self.root_account_id ||= self.parent_account_id
2011-02-01 09:57:29 +08:00
self.parent_account_id ||= self.root_account_id
Account.invalidate_cache(self.id) if self.id
@should_update_account_associations = self.parent_account_id_changed? || self.root_account_id_changed?
true
end
def update_account_associations_if_changed
send_later_if_production(:update_account_associations) if @should_update_account_associations
end
def equella_settings
endpoint = self.settings[:equella_endpoint] || self.equella_endpoint
if !endpoint.blank?
2011-02-01 09:57:29 +08:00
OpenObject.new({
:endpoint => endpoint,
:default_action => self.settings[:equella_action] || 'selectOrAdd',
:teaser => self.settings[:equella_teaser]
2011-02-01 09:57:29 +08:00
})
else
nil
2011-02-01 09:57:29 +08:00
end
end
def settings
result = self.read_attribute(:settings)
return result if result
return self.write_attribute(:settings, {}) unless frozen?
{}.freeze
2011-02-01 09:57:29 +08:00
end
def domain
HostUrl.context_host(self)
end
def root_account?
!self.root_account_id
end
def root_account_with_self
return self if self.root_account?
root_account_without_self
end
alias_method_chain :root_account, :self
def sub_accounts_as_options(indent = 0, preloaded_accounts = nil)
unless preloaded_accounts
preloaded_accounts = {}
self.root_account.all_accounts.active.each do |account|
(preloaded_accounts[account.parent_account_id] ||= []) << account
end
end
res = [[("&nbsp;&nbsp;" * indent).html_safe + self.name, self.id]]
if preloaded_accounts[self.id]
preloaded_accounts[self.id].each do |account|
res += account.sub_accounts_as_options(indent + 1, preloaded_accounts)
end
2011-02-01 09:57:29 +08:00
end
res
end
def users_name_like(query="")
@cached_users_name_like ||= {}
@cached_users_name_like[query] ||= self.fast_all_users.name_like(query)
end
def fast_course_base(opts)
columns = "courses.id, courses.name, courses.workflow_state, courses.course_code, courses.sis_source_id, courses.enrollment_term_id"
associated_courses = self.associated_courses.active
associated_courses = associated_courses.with_enrollments if opts[:hide_enrollmentless_courses]
associated_courses = associated_courses.for_term(opts[:term]) if opts[:term].present?
associated_courses = yield associated_courses if block_given?
associated_courses.limit(opts[:limit]).active_first.find(:all, :select => columns, :group => columns)
end
def fast_all_courses(opts={})
2011-02-01 09:57:29 +08:00
@cached_fast_all_courses ||= {}
@cached_fast_all_courses[opts] ||= self.fast_course_base(opts)
2011-02-01 09:57:29 +08:00
end
2011-02-01 09:57:29 +08:00
def all_users(limit=250)
@cached_all_users ||= {}
@cached_all_users[limit] ||= User.of_account(self).scoped(:limit=>limit)
end
def fast_all_users(limit=nil)
@cached_fast_all_users ||= {}
@cached_fast_all_users[limit] ||= self.all_users(limit).active.order_by_sortable_name.scoped(:select => "users.id, users.name, users.sortable_name")
2011-02-01 09:57:29 +08:00
end
def users_not_in_groups_sql(groups, opts={})
["SELECT u.id, u.name
FROM users u
INNER JOIN user_account_associations uaa on uaa.user_id = u.id
WHERE uaa.account_id = ? AND u.workflow_state != 'deleted'
#{"AND NOT EXISTS (SELECT *
FROM group_memberships gm
WHERE gm.user_id = u.id AND
gm.group_id IN (#{groups.map(&:id).join ','}))" unless groups.empty?}
#{"ORDER BY #{opts[:order_by]}" if opts[:order_by].present?}", self.id]
end
def users_not_in_groups(groups)
User.find_by_sql(users_not_in_groups_sql(groups))
end
2011-02-01 09:57:29 +08:00
def paginate_users_not_in_groups(groups, page, per_page = 15)
User.paginate_by_sql(users_not_in_groups_sql(groups, :order_by => "#{User.sortable_name_order_by_clause('u')} ASC"),
:page => page, :per_page => per_page)
2011-02-01 09:57:29 +08:00
end
def courses_name_like(query="", opts={})
opts[:limit] ||= 200
@cached_courses_name_like ||= {}
@cached_courses_name_like[[query, opts]] ||= self.fast_course_base(opts) {|q| q.name_like(query)}
2011-02-01 09:57:29 +08:00
end
self enrollment refactor to facilitate CN integration fixes #CNVS-1119, potentially supersedes https://gerrit.instructure.com/14501 with a little work. simpler flow that is more consistent with FFT signup. whether you click the "join course" button (popup) or go to the join url, the workflow is the same: 1. if you are authenticated, you just click the enroll button. 2. if you are not authenticated, you can either: 1. enter your (canvas/ldap) credentials and submit to join the course. 2. register and join the course (single form). you will then be dropped on the course dashboard in the pre_registered state just like a /register signup (you have to follow the link in your email to set a password). note that if open registration is turned off, option 2.2 is not available. other items of interest: * fix CSRF vulnerabilities where you can enroll authenticated users in open courses, or un-enroll them if you know their enrollment's UUID * move to shorter course-id-less route (w/ join code) * reuse UserController#create * handy openAsDialog behavior and embedded view mode * better json support in PseudonymSessionsController#create * extract markdown helper from mt * show "you need to confirm your email" popup when you land on the course page the first time (already showed on dashboard) test plan: 1. test the authenticated/unauthenticated scenarios above, for both the popup and join pages 2. regression test of /registration forms Change-Id: I0d8351695356d437bdbba72cb66c23ed268b0d1a Reviewed-on: https://gerrit.instructure.com/15902 Tested-by: Jenkins <jenkins@instructure.com> Reviewed-by: Joe Tanner <joe@instructure.com> QA-Review: Jon Jensen <jon@instructure.com>
2012-12-07 14:28:37 +08:00
def self_enrollment_course_for(code)
all_courses.
where(:self_enrollment => true, :self_enrollment_code => code).
first
end
2011-02-01 09:57:29 +08:00
def file_namespace
Shard.default.activate { "account_#{self.root_account.id}" }
2011-02-01 09:57:29 +08:00
end
def self.account_lookup_cache_key(id)
['_account_lookup2', id].cache_key
2011-02-01 09:57:29 +08:00
end
def self.invalidate_cache(id)
Rails.cache.delete(account_lookup_cache_key(id)) if id
rescue
nil
end
def quota
Rails.cache.fetch(['current_quota', self].cache_key) do
read_attribute(:storage_quota) ||
(self.parent_account.default_storage_quota rescue nil) ||
Setting.get_cached('account_default_quota', 500.megabytes.to_s).to_i
2011-02-01 09:57:29 +08:00
end
end
def default_storage_quota
read_attribute(:default_storage_quota) ||
(self.parent_account.default_storage_quota rescue nil) ||
Setting.get_cached('account_default_quota', 500.megabytes.to_s).to_i
end
def default_storage_quota_mb
default_storage_quota / 1.megabyte
end
def default_storage_quota_mb=(val)
self.default_storage_quota = val.try(:to_i).try(:megabytes)
2011-02-01 09:57:29 +08:00
end
def default_storage_quota=(val)
val = val.to_f
val = nil if val <= 0
# If the value is the same as the inherited value, then go
# ahead and blank it so it keeps using the inherited value
if parent_account && parent_account.default_storage_quota == val
val = nil
end
write_attribute(:default_storage_quota, val)
end
def default_user_storage_quota
read_attribute(:default_user_storage_quota) ||
User.default_storage_quota
end
def default_user_storage_quota=(val)
val = val.to_i
val = nil if val == User.default_storage_quota || val <= 0
write_attribute(:default_user_storage_quota, val)
end
def default_user_storage_quota_mb
default_user_storage_quota / 1.megabyte
end
2011-02-01 09:57:29 +08:00
def default_user_storage_quota_mb=(val)
self.default_user_storage_quota = val.try(:to_i).try(:megabytes)
end
2011-02-01 09:57:29 +08:00
def turnitin_shared_secret=(secret)
return if secret.blank?
self.turnitin_crypted_secret, self.turnitin_salt = Canvas::Security.encrypt_password(secret, 'instructure_turnitin_secret_shared')
end
def turnitin_shared_secret
return nil unless self.turnitin_salt && self.turnitin_crypted_secret
Canvas::Security.decrypt_password(self.turnitin_crypted_secret, self.turnitin_salt, 'instructure_turnitin_secret_shared')
end
def account_chain(opts = {})
2011-02-01 09:57:29 +08:00
res = [self]
if ActiveRecord::Base.configurations[RAILS_ENV]['adapter'] == 'postgresql'
res.concat Account.find_by_sql(<<-SQL) if self.parent_account_id
WITH RECURSIVE t AS (
SELECT * FROM accounts WHERE id=#{self.parent_account_id}
UNION
SELECT accounts.* FROM accounts INNER JOIN t ON accounts.id=t.parent_account_id
)
SELECT * FROM t
SQL
else
account = self
while account.parent_account
account = account.parent_account
res << account
end
2011-02-01 09:57:29 +08:00
end
res << self.root_account unless res.map(&:id).include?(self.root_account_id)
res << Account.site_admin if opts[:include_site_admin] && !self.site_admin?
res.compact
2011-02-01 09:57:29 +08:00
end
def account_chain_loop
# this record hasn't been saved to the db yet, so if the the chain includes
# this account, it won't point to the new parent yet, and should still be
# valid
if self.parent_account.account_chain_ids.include?(self.id)
errors.add(:parent_account_id,
"Setting account #{self.sis_source_id || self.id}'s parent to #{self.parent_account.sis_source_id || self.parent_account_id} would create a loop")
end
end
def associated_accounts
self.account_chain
end
2011-02-01 09:57:29 +08:00
def account_chain_ids(opts={})
account_chain(opts).map(&:id)
end
memoize :account_chain_ids
2011-02-01 09:57:29 +08:00
def membership_for_user(user)
self.account_users.find_by_user_id(user && user.id)
end
Add new permissions management ui (role overrides) When editing permissions for an account, course or admin section the UI has changed to use drop downs instead of a 6 state check box. It has also been switched to use 100% backbonejs to handle creating and editing roles. This works with the roles api and allows you to create custom roles for courses. fixes #CNVS-1165 Test Plan Apply this test in two places. The Site Admin and a custom university. 1. Go to the "permissions" tab 2. Notice permissions are available per role in a table. ------------ Adding/Removing Roles -------------- 1. When under the "Account Role" tab click "Add Role" 2. Enter a role name and click "Create/Add" 3. A new role should appear automatically. 4. You should be able to delete this role by clicking the x next to it's name. ------------ Editing Permissions ---------------- 1. Go to a role in the permissions tab. 2. Try to edit one of it's permissions. It should have a drop down with options to select permissions. You should be able to enable/disable or set to default and lock the permission. Read only permissions cannot be clicked on. 3. Buttons with default set should have a special "default" class added to the button representing its permission. ------------ Organization of Roles -------------- 1. In the course role tab, add a few roles with different base types (use the dropwdown) 2. Roles should be grouped together by base role type. ------------ Automatic Saving ------------------ 1. Change a permission on a role 2. The role should automatically save the the permission after selecting the option you want Change-Id: I343afc36b85183e5913c8eef6111ea2c5ae62726 Reviewed-on: https://gerrit.instructure.com/16323 Tested-by: Jenkins <jenkins@instructure.com> Reviewed-by: Bracken Mosbacker <bracken@instructure.com> QA-Review: Adam Phillipps <adam@instructure.com>
2012-12-27 04:09:58 +08:00
def available_custom_account_roles
account_roles = roles.for_accounts.active
account_roles |= self.parent_account.available_custom_account_roles if self.parent_account
account_roles
end
def available_account_roles
account_roles = roles.for_accounts.active.map(&:name)
account_roles |= ['AccountAdmin']
account_roles |= self.parent_account.available_account_roles if self.parent_account
account_roles
2011-02-01 09:57:29 +08:00
end
def available_course_roles(include_inactive=false)
available_course_roles_by_name(include_inactive).keys
end
def available_course_roles_by_name(include_inactive=false)
scope = include_inactive ? roles.for_courses.not_deleted : roles.for_courses.active
role_map = {}
scope.each { |role| role_map[role.name] = role }
role_map.reverse_merge!(parent_account.available_course_roles_by_name(include_inactive)) if parent_account
role_map
end
def get_course_role(role_name)
course_role = self.roles.for_courses.not_deleted.find_by_name(role_name)
course_role ||= self.parent_account.get_course_role(role_name) if self.parent_account
course_role
end
def has_role?(role_name)
roles.not_deleted.scoped(:conditions => ["roles.name = ?", role_name]).any?
end
def find_role(role_name)
roles.not_deleted.find_by_name(role_name) || (parent_account && parent_account.find_role(role_name))
end
def account_authorization_config
# We support multiple auth configs per account, but several places we assume there is only one.
# This is for compatibility with those areas. TODO: migrate everything to supporting multiple
# auth configs
self.account_authorization_configs.first
end
def login_handle_name_is_customized?
self.account_authorization_config && self.account_authorization_config.login_handle_name.present?
end
2011-02-01 09:57:29 +08:00
def login_handle_name
login_handle_name_is_customized? ? self.account_authorization_config.login_handle_name :
(self.delegated_authentication? ? AccountAuthorizationConfig.default_delegated_login_handle_name :
AccountAuthorizationConfig.default_login_handle_name)
2011-02-01 09:57:29 +08:00
end
def self_and_all_sub_accounts
@self_and_all_sub_accounts ||= Account.connection.select_all("SELECT id FROM accounts WHERE accounts.root_account_id = #{self.id} OR accounts.parent_account_id = #{self.id}").map{|ref| ref['id'].to_i}.uniq + [self.id]
2011-02-01 09:57:29 +08:00
end
def default_time_zone
read_attribute(:default_time_zone) || "Mountain Time (US & Canada)"
end
workflow do
state :active
state :deleted
end
def account_users_for(user)
return [] unless user
@account_users_cache ||= {}
if self == Account.site_admin
@account_users_cache[user] ||= Rails.cache.fetch('all_site_admin_account_users', :expires_in => 30.minutes) do
self.account_users.all
end.select { |au| au.user_id == user.id }.each { |au| au.account = self }
else
@account_chain_ids ||= self.account_chain(:include_site_admin => true).map { |a| a.active? ? a.id : nil }.compact
@account_users_cache[user] ||= Shard.partition_by_shard(@account_chain_ids) do |account_chain_ids|
if account_chain_ids == [Account.site_admin.id]
Account.site_admin.account_users_for(user)
else
AccountUser.find(:all, :conditions => { :account_id => account_chain_ids, :user_id => user.id })
end
end
end
@account_users_cache[user] ||= []
@account_users_cache[user]
end
# returns all account users for this entire account tree
def all_account_users_for(user)
raise "must be a root account" unless self.root_account?
Shard.partition_by_shard([self, Account.site_admin].uniq) do |accounts|
ids = accounts.map(&:id)
AccountUser.find(:all, :include => :account, :joins => :account, :conditions => ["user_id=? AND (root_account_id IN (?) OR account_id IN (?))", user.id, ids, ids])
end
end
2011-02-01 09:57:29 +08:00
set_policy do
enrollment_types = RoleOverride.enrollment_types.map { |role| role[:name] }
RoleOverride.permissions.each do |permission, details|
given { |user| self.account_users_for(user).any? { |au| au.has_permission_to?(permission) && (!details[:if] || send(details[:if])) } }
can permission
can :create_courses if permission == :manage_courses
next unless details[:account_only]
((details[:available_to] | details[:true_for]) & enrollment_types).each do |role|
given { |user| user && RoleOverride.permission_for(self, permission, role)[:enabled] &&
self.course_account_associations.find(:first, :joins => 'INNER JOIN enrollments ON course_account_associations.course_id=enrollments.course_id',
:conditions => ["enrollments.type=? AND enrollments.workflow_state IN ('active', 'completed') AND user_id=?", role, user.id]) &&
(!details[:if] || send(details[:if])) }
can permission
end
2011-02-01 09:57:29 +08:00
end
given { |user| !self.account_users_for(user).empty? }
can :read and can :manage and can :update and can :delete and can :read_outcomes
given { |user|
root_account = self.root_account
result = false
site_admin = self.site_admin?
refactor user creation/invitations closes #5833 fixes #5573, #5572, #5753 * communication channels are now only unique within a single user * UserList changes * Always resolve pseudonym#unique_ids * Support looking up by SMS CCs * Option to either require e-mails match an existing CC, or e-mails that don't match a Pseudonym will always be returned unattached (relying on better merging behavior to not have a gazillion accounts created) * Method to return users, creating new ones (*without* a Pseudonym) if necessary. (can't create with a pseudonym, since Pseudonym#unique_id is still unique, I can't have multiple outstanding users with the same unique_id) * EnrollmentsFromUserList is mostly gutted, now using UserList's functionality directy. * Use UserList for adding account admins, removing the now unused Account#add_admin => User#find_by_email/User#assert_by_email codepath * Update UsersController#create to not worry about duplicate communication channels * Remove AccountsController#add_user, and just use UsersController#create * Change SIS::UserImporter to send out a merge opportunity e-mail if a conflicting CC is found (but still create the CC) * In /profile, don't worry about conflicting CCs (the CC confirmation process will now allow merging) * Remove CommunicationChannelsController#try_merge and #merge * For the non-simple case of CoursesController#enrollment_invitation redirect to /register (CommunicationsChannelController#confirm) * Remove CoursesController#transfer_enrollment * Move PseudonymsController#registration_confirmation to CommunicationChannelsController#confirm (have to be able to register an account without a Pseudonym yet) * Fold the old direct confirm functionality in, if there are no available merge opportunities * Allow merging the new account with the currently logged in user * Allow changing the Pseudonym#unique_id when registering a new account (since there might be conflicts) * Display a list of merge opportunities based on conflicting communication channels * Provide link(s) to log in as the other user, redirecting back to the registration page after login is complete (to complete the merge as the current user) * Remove several assert_* methods that are no longer needed * Update PseudonymSessionsController a bit to deal with the new way of dealing with conflicting CCs (especially CCs from LDAP), and to redirect back to the registration/confirmation page when attempting to do a merge * Expose the open_registration setting; use it to control if inviting users to a course is able to create new users Change-Id: If2f38818a71af656854d3bf8431ddbf5dcb84691 Reviewed-on: https://gerrit.instructure.com/6149 Tested-by: Hudson <hudson@instructure.com> Reviewed-by: Jacob Fugal <jacob@instructure.com>
2011-10-13 04:30:48 +08:00
if !site_admin && user && root_account.teachers_can_create_courses?
count = user.enrollments.scoped(:select=>'id', :conditions=>"enrollments.type IN ('TeacherEnrollment', 'DesignerEnrollment') AND (enrollments.workflow_state != 'deleted') AND root_account_id = #{root_account.id}").count
result = true if count > 0
end
refactor user creation/invitations closes #5833 fixes #5573, #5572, #5753 * communication channels are now only unique within a single user * UserList changes * Always resolve pseudonym#unique_ids * Support looking up by SMS CCs * Option to either require e-mails match an existing CC, or e-mails that don't match a Pseudonym will always be returned unattached (relying on better merging behavior to not have a gazillion accounts created) * Method to return users, creating new ones (*without* a Pseudonym) if necessary. (can't create with a pseudonym, since Pseudonym#unique_id is still unique, I can't have multiple outstanding users with the same unique_id) * EnrollmentsFromUserList is mostly gutted, now using UserList's functionality directy. * Use UserList for adding account admins, removing the now unused Account#add_admin => User#find_by_email/User#assert_by_email codepath * Update UsersController#create to not worry about duplicate communication channels * Remove AccountsController#add_user, and just use UsersController#create * Change SIS::UserImporter to send out a merge opportunity e-mail if a conflicting CC is found (but still create the CC) * In /profile, don't worry about conflicting CCs (the CC confirmation process will now allow merging) * Remove CommunicationChannelsController#try_merge and #merge * For the non-simple case of CoursesController#enrollment_invitation redirect to /register (CommunicationsChannelController#confirm) * Remove CoursesController#transfer_enrollment * Move PseudonymsController#registration_confirmation to CommunicationChannelsController#confirm (have to be able to register an account without a Pseudonym yet) * Fold the old direct confirm functionality in, if there are no available merge opportunities * Allow merging the new account with the currently logged in user * Allow changing the Pseudonym#unique_id when registering a new account (since there might be conflicts) * Display a list of merge opportunities based on conflicting communication channels * Provide link(s) to log in as the other user, redirecting back to the registration page after login is complete (to complete the merge as the current user) * Remove several assert_* methods that are no longer needed * Update PseudonymSessionsController a bit to deal with the new way of dealing with conflicting CCs (especially CCs from LDAP), and to redirect back to the registration/confirmation page when attempting to do a merge * Expose the open_registration setting; use it to control if inviting users to a course is able to create new users Change-Id: If2f38818a71af656854d3bf8431ddbf5dcb84691 Reviewed-on: https://gerrit.instructure.com/6149 Tested-by: Hudson <hudson@instructure.com> Reviewed-by: Jacob Fugal <jacob@instructure.com>
2011-10-13 04:30:48 +08:00
if !site_admin && user && !result && root_account.students_can_create_courses?
count = user.enrollments.scoped(:select=>'id', :conditions=>"enrollments.type IN ('StudentEnrollment', 'ObserverEnrollment') AND (enrollments.workflow_state != 'deleted') AND root_account_id = #{root_account.id}").count
result = true if count > 0
end
refactor user creation/invitations closes #5833 fixes #5573, #5572, #5753 * communication channels are now only unique within a single user * UserList changes * Always resolve pseudonym#unique_ids * Support looking up by SMS CCs * Option to either require e-mails match an existing CC, or e-mails that don't match a Pseudonym will always be returned unattached (relying on better merging behavior to not have a gazillion accounts created) * Method to return users, creating new ones (*without* a Pseudonym) if necessary. (can't create with a pseudonym, since Pseudonym#unique_id is still unique, I can't have multiple outstanding users with the same unique_id) * EnrollmentsFromUserList is mostly gutted, now using UserList's functionality directy. * Use UserList for adding account admins, removing the now unused Account#add_admin => User#find_by_email/User#assert_by_email codepath * Update UsersController#create to not worry about duplicate communication channels * Remove AccountsController#add_user, and just use UsersController#create * Change SIS::UserImporter to send out a merge opportunity e-mail if a conflicting CC is found (but still create the CC) * In /profile, don't worry about conflicting CCs (the CC confirmation process will now allow merging) * Remove CommunicationChannelsController#try_merge and #merge * For the non-simple case of CoursesController#enrollment_invitation redirect to /register (CommunicationsChannelController#confirm) * Remove CoursesController#transfer_enrollment * Move PseudonymsController#registration_confirmation to CommunicationChannelsController#confirm (have to be able to register an account without a Pseudonym yet) * Fold the old direct confirm functionality in, if there are no available merge opportunities * Allow merging the new account with the currently logged in user * Allow changing the Pseudonym#unique_id when registering a new account (since there might be conflicts) * Display a list of merge opportunities based on conflicting communication channels * Provide link(s) to log in as the other user, redirecting back to the registration page after login is complete (to complete the merge as the current user) * Remove several assert_* methods that are no longer needed * Update PseudonymSessionsController a bit to deal with the new way of dealing with conflicting CCs (especially CCs from LDAP), and to redirect back to the registration/confirmation page when attempting to do a merge * Expose the open_registration setting; use it to control if inviting users to a course is able to create new users Change-Id: If2f38818a71af656854d3bf8431ddbf5dcb84691 Reviewed-on: https://gerrit.instructure.com/6149 Tested-by: Hudson <hudson@instructure.com> Reviewed-by: Jacob Fugal <jacob@instructure.com>
2011-10-13 04:30:48 +08:00
if !site_admin && user && !result && root_account.no_enrollments_can_create_courses?
count = user.enrollments.scoped(:select=>'id', :conditions=>"enrollments.workflow_state != 'deleted' AND root_account_id = #{root_account.id}").count
result = true if count == 0
end
result
}
can :create_courses
# any logged in user can read global outcomes, but must be checked against the site admin
given{ |user,session| self.site_admin? && user }
can :read_global_outcomes
# any user with an association to this account can read the outcomes in the account
given{ |user,session| user && self.user_account_associations.find_by_user_id(user.id) }
can :read_outcomes
2011-02-01 09:57:29 +08:00
end
alias_method :destroy!, :destroy
def destroy
self.workflow_state = 'deleted'
self.deleted_at = Time.now
save!
end
2011-02-01 09:57:29 +08:00
def to_atom
Atom::Entry.new do |entry|
entry.title = self.name
entry.updated = self.updated_at
entry.published = self.created_at
entry.links << Atom::Link.new(:rel => 'alternate',
:href => "/accounts/#{self.id}")
end
end
def default_enrollment_term
return @default_enrollment_term if @default_enrollment_term
if self.root_account?
@default_enrollment_term = self.enrollment_terms.active.find_or_create_by_name(EnrollmentTerm::DEFAULT_TERM_NAME)
end
2011-02-01 09:57:29 +08:00
end
def add_user(user, membership_type = nil)
return nil unless user && user.is_a?(User)
membership_type ||= 'AccountAdmin'
au = self.account_users.find_by_user_id_and_membership_type(user.id, membership_type)
au ||= self.account_users.create(:user => user, :membership_type => membership_type)
2011-02-01 09:57:29 +08:00
end
def context_code
raise "DONT USE THIS, use .short_name instead" unless ENV['RAILS_ENV'] == "production"
end
def short_name
name
end
self enrollment refactor to facilitate CN integration fixes #CNVS-1119, potentially supersedes https://gerrit.instructure.com/14501 with a little work. simpler flow that is more consistent with FFT signup. whether you click the "join course" button (popup) or go to the join url, the workflow is the same: 1. if you are authenticated, you just click the enroll button. 2. if you are not authenticated, you can either: 1. enter your (canvas/ldap) credentials and submit to join the course. 2. register and join the course (single form). you will then be dropped on the course dashboard in the pre_registered state just like a /register signup (you have to follow the link in your email to set a password). note that if open registration is turned off, option 2.2 is not available. other items of interest: * fix CSRF vulnerabilities where you can enroll authenticated users in open courses, or un-enroll them if you know their enrollment's UUID * move to shorter course-id-less route (w/ join code) * reuse UserController#create * handy openAsDialog behavior and embedded view mode * better json support in PseudonymSessionsController#create * extract markdown helper from mt * show "you need to confirm your email" popup when you land on the course page the first time (already showed on dashboard) test plan: 1. test the authenticated/unauthenticated scenarios above, for both the popup and join pages 2. regression test of /registration forms Change-Id: I0d8351695356d437bdbba72cb66c23ed268b0d1a Reviewed-on: https://gerrit.instructure.com/15902 Tested-by: Jenkins <jenkins@instructure.com> Reviewed-by: Joe Tanner <joe@instructure.com> QA-Review: Jon Jensen <jon@instructure.com>
2012-12-07 14:28:37 +08:00
# can be set/overridden by plugin to enforce email pseudonyms
attr_accessor :email_pseudonyms
2011-02-01 09:57:29 +08:00
def password_authentication?
2011-02-01 09:57:29 +08:00
!!(!self.account_authorization_config || self.account_authorization_config.password_authentication?)
end
def delegated_authentication?
!canvas_authentication? || !!(self.account_authorization_config && self.account_authorization_config.delegated_authentication?)
end
def forgot_password_external_url
account_authorization_config.try(:change_password_url)
end
def cas_authentication?
!!(self.account_authorization_config && self.account_authorization_config.cas_authentication?)
end
2011-02-01 09:57:29 +08:00
def ldap_authentication?
self.account_authorization_configs.any? { |aac| aac.ldap_authentication? }
2011-02-01 09:57:29 +08:00
end
2011-02-01 09:57:29 +08:00
def saml_authentication?
!!(self.account_authorization_config && self.account_authorization_config.saml_authentication?)
end
def multi_auth?
self.account_authorization_configs.count > 1
end
def auth_discovery_url=(url)
self.settings[:auth_discovery_url] = url
end
def auth_discovery_url
self.settings[:auth_discovery_url]
end
def validate_auth_discovery_url
return if self.settings[:auth_discovery_url].blank?
begin
value, uri = CustomValidations.validate_url(self.settings[:auth_discovery_url])
self.auth_discovery_url = value
rescue URI::InvalidURIError, ArgumentError
errors.add(:discovery_url, t('errors.invalid_discovery_url', "The discovery URL is not valid" ))
end
end
2011-02-01 09:57:29 +08:00
def find_courses(string)
self.all_courses.select{|c| c.name.match(string) }
end
def find_users(string)
self.pseudonyms.map{|p| p.user }.select{|u| u.name.match(string) }
end
def self.site_admin
get_special_account(:site_admin, 'Site Admin')
2011-02-01 09:57:29 +08:00
end
def self.default
get_special_account(:default, 'Default Account')
end
def self.clear_special_account_cache!
@special_accounts = {}
2011-02-01 09:57:29 +08:00
end
# an opportunity for plugins to load some other stuff up before caching the account
def precache
end
2011-02-01 09:57:29 +08:00
def self.find_cached(id)
account = Rails.cache.fetch(account_lookup_cache_key(id), :expires_in => 1.hour) do
account = Account.find_by_id(id)
account.precache if account
account || :nil
end
account = nil if account == :nil
account
end
def self.get_special_account(special_account_type, default_account_name)
Shard.default.activate do
@special_account_ids ||= {}
@special_accounts ||= {}
account = @special_accounts[special_account_type]
unless account
special_account_id = @special_account_ids[special_account_type] ||= Setting.get("#{special_account_type}_account_id", nil)
account = @special_accounts[special_account_type] = Account.find_cached(special_account_id) if special_account_id
end
# another process (i.e. selenium spec) may have changed the setting
unless account
special_account_id = Setting.get("#{special_account_type}_account_id", nil)
if special_account_id && special_account_id != @special_account_ids[special_account_type]
@special_account_ids[special_account_type] = special_account_id
account = @special_accounts[special_account_type] = Account.find_by_id(special_account_id)
end
end
unless account
# TODO i18n
t '#account.default_site_administrator_account_name', 'Site Admin'
t '#account.default_account_name', 'Default Account'
account = @special_accounts[special_account_type] = Account.create!(:name => default_account_name)
Setting.set("#{special_account_type}_account_id", account.id)
@special_account_ids[special_account_type] = account.id
end
account
end
2011-02-01 09:57:29 +08:00
end
def site_admin?
self == Account.site_admin
2011-02-01 09:57:29 +08:00
end
def display_name
self.name
end
# Updates account associations for all the courses and users associated with this account
def update_account_associations
account_chain_cache = {}
2011-02-01 09:57:29 +08:00
all_user_ids = []
all_user_ids += Course.update_account_associations(self.associated_courses, :skip_user_account_associations => true, :account_chain_cache => account_chain_cache)
2011-02-01 09:57:29 +08:00
# Make sure we have all users with existing account associations.
# (This should catch users with Pseudonyms associated with the account.)
all_user_ids += UserAccountAssociation.scoped(:select => 'user_id', :conditions => { :account_id => self.id }).map(&:user_id)
2011-02-01 09:57:29 +08:00
# Update the users' associations as well
User.update_account_associations(all_user_ids.uniq, :account_chain_cache => account_chain_cache)
2011-02-01 09:57:29 +08:00
end
# this will take an account and make it a sub_account of
# itself. Also updates all it's descendant accounts to point to
# the correct root account, and updates the pseudonyms to
# points to the new root account as well.
def consume_account(account)
account.all_accounts.each do |sub_account|
sub_account.root_account = self.root_account
2011-02-01 09:57:29 +08:00
sub_account.save!
end
account.parent_account = self
account.root_account = self.root_account
2011-02-01 09:57:29 +08:00
account.save!
account.pseudonyms.each do |pseudonym|
pseudonym.account = self.root_account
2011-02-01 09:57:29 +08:00
pseudonym.save!
end
end
2011-02-01 09:57:29 +08:00
def course_count
self.child_courses.not_deleted.count('DISTINCT course_id')
2011-02-01 09:57:29 +08:00
end
memoize :course_count
def sub_account_count
self.sub_accounts.active.count
end
memoize :sub_account_count
def user_count
self.user_account_associations.count
end
memoize :user_count
2011-02-01 09:57:29 +08:00
def current_sis_batch
if (current_sis_batch_id = self.read_attribute(:current_sis_batch_id)) && current_sis_batch_id.present?
self.sis_batches.find_by_id(current_sis_batch_id)
end
2011-02-01 09:57:29 +08:00
end
def turnitin_settings
if self.turnitin_account_id && self.turnitin_shared_secret && !self.turnitin_account_id.empty? && !self.turnitin_shared_secret.empty?
[self.turnitin_account_id, self.turnitin_shared_secret]
else
self.parent_account.turnitin_settings rescue nil
end
end
def closest_turnitin_pledge
if self.turnitin_pledge && !self.turnitin_pledge.empty?
self.turnitin_pledge
else
res = self.parent_account.turnitin_pledge
res ||= t('#account.turnitin_pledge', "This assignment submission is my own, original work")
2011-02-01 09:57:29 +08:00
end
end
def closest_turnitin_comments
if self.turnitin_comments && !self.turnitin_comments.empty?
self.turnitin_comments
else
self.parent_account.closest_turnitin_comments rescue nil
2011-02-01 09:57:29 +08:00
end
end
def self_enrollment_allowed?(course)
if !settings[:self_enrollment].blank?
!!(settings[:self_enrollment] == 'any' || (!course.sis_source_id && settings[:self_enrollment] == 'manually_created'))
else
!!(parent_account && parent_account.self_enrollment_allowed?(course))
end
end
2011-02-01 09:57:29 +08:00
TAB_COURSES = 0
TAB_STATISTICS = 1
TAB_PERMISSIONS = 2
TAB_SUB_ACCOUNTS = 3
TAB_TERMS = 4
TAB_AUTHENTICATION = 5
TAB_USERS = 6
TAB_OUTCOMES = 7
TAB_RUBRICS = 8
TAB_SETTINGS = 9
TAB_FACULTY_JOURNAL = 10
TAB_SIS_IMPORT = 11
TAB_GRADING_STANDARDS = 12
TAB_QUESTION_BANKS = 13
# site admin tabs
TAB_PLUGINS = 14
TAB_JOBS = 15
TAB_DEVELOPER_KEYS = 16
basic lti navigation links By properly configuring external tools (see /spec/models/course_spec/rb:898 for examples) they can be added as left-side navigation links to a course, an account, or to the user profile section of Canvas. testing notes: - you have to manually set options on the external tool: - for user navigation the tool needs to be created on the root account with the following settings: {:user_navigation => {:url => <url>, :text => <tab label>} } (there are also some optional language options you can set using the :labels attribute) - for account navigation it's the same - for course navigation it's the same, except with :course_navigation there's also some additional options: :visibility => <value> // public, members, admins :default => <value> // disabled, enabled test plan: - configure a user navigation tool at the root account level, make sure it shows up in the user's profile section - configure a course navigation tool at the account level, make sure it shows up in the course's navigation - configure a course navigation tool at the course level, make sure it shows up in the course's navigation - make sure :default => 'disabled' course navigation tools don't appear by default in the navigation, but can be enabled on the course settings page - make sure :visibility => 'members' only shows up for course members - make sure :visibility => 'admins' only shows up for course admins - configure an account navigation tool at the account level, make sure it shows up in the account's navigation, and any sub-account's navigation Change-Id: I977da3c6b89a9e32b4cff4c2b6b221f8162782ff Reviewed-on: https://gerrit.instructure.com/5427 Reviewed-by: Brian Whitmer <brian@instructure.com> Tested-by: Hudson <hudson@instructure.com>
2011-08-18 13:49:01 +08:00
def external_tool_tabs(opts)
tools = ContextExternalTool.active.find_all_for(self, :account_navigation)
basic lti navigation links By properly configuring external tools (see /spec/models/course_spec/rb:898 for examples) they can be added as left-side navigation links to a course, an account, or to the user profile section of Canvas. testing notes: - you have to manually set options on the external tool: - for user navigation the tool needs to be created on the root account with the following settings: {:user_navigation => {:url => <url>, :text => <tab label>} } (there are also some optional language options you can set using the :labels attribute) - for account navigation it's the same - for course navigation it's the same, except with :course_navigation there's also some additional options: :visibility => <value> // public, members, admins :default => <value> // disabled, enabled test plan: - configure a user navigation tool at the root account level, make sure it shows up in the user's profile section - configure a course navigation tool at the account level, make sure it shows up in the course's navigation - configure a course navigation tool at the course level, make sure it shows up in the course's navigation - make sure :default => 'disabled' course navigation tools don't appear by default in the navigation, but can be enabled on the course settings page - make sure :visibility => 'members' only shows up for course members - make sure :visibility => 'admins' only shows up for course admins - configure an account navigation tool at the account level, make sure it shows up in the account's navigation, and any sub-account's navigation Change-Id: I977da3c6b89a9e32b4cff4c2b6b221f8162782ff Reviewed-on: https://gerrit.instructure.com/5427 Reviewed-by: Brian Whitmer <brian@instructure.com> Tested-by: Hudson <hudson@instructure.com>
2011-08-18 13:49:01 +08:00
tools.sort_by(&:id).map do |tool|
{
:id => tool.asset_string,
:label => tool.label_for(:account_navigation, opts[:language]),
:css_class => tool.asset_string,
:href => :account_external_tool_path,
:external => true,
:args => [self.id, tool.id]
}
end
end
2011-02-01 09:57:29 +08:00
def tabs_available(user=nil, opts={})
manage_settings = user && self.grants_right?(user, nil, :manage_account_settings)
if site_admin?
tabs = []
tabs << { :id => TAB_USERS, :label => t('#account.tab_users', "Users"), :css_class => 'users', :href => :account_users_path } if user && self.grants_right?(user, nil, :read_roster)
tabs << { :id => TAB_PERMISSIONS, :label => t('#account.tab_permissions', "Permissions"), :css_class => 'permissions', :href => :account_permissions_path } if user && self.grants_right?(user, nil, :manage_role_overrides)
tabs << { :id => TAB_AUTHENTICATION, :label => t('#account.tab_authentication', "Authentication"), :css_class => 'authentication', :href => :account_account_authorization_configs_path } if manage_settings
tabs << { :id => TAB_PLUGINS, :label => t("#account.tab_plugins", "Plugins"), :css_class => "plugins", :href => :plugins_path, :no_args => true } if self.grants_right?(user, nil, :manage_site_settings)
tabs << { :id => TAB_JOBS, :label => t("#account.tab_jobs", "Jobs"), :css_class => "jobs", :href => :jobs_path, :no_args => true } if self.grants_right?(user, nil, :manage_jobs)
tabs << { :id => TAB_DEVELOPER_KEYS, :label => t("#account.tab_developer_keys", "Developer Keys"), :css_class => "developer_keys", :href => :developer_keys_path, :no_args => true } if self.grants_right?(user, nil, :manage_developer_keys)
else
tabs = []
tabs << { :id => TAB_COURSES, :label => t('#account.tab_courses', "Courses"), :css_class => 'courses', :href => :account_path } if user && self.grants_right?(user, nil, :read_course_list)
tabs << { :id => TAB_USERS, :label => t('#account.tab_users', "Users"), :css_class => 'users', :href => :account_users_path } if user && self.grants_right?(user, nil, :read_roster)
tabs << { :id => TAB_STATISTICS, :label => t('#account.tab_statistics', "Statistics"), :css_class => 'statistics', :href => :statistics_account_path } if user && self.grants_right?(user, nil, :view_statistics)
tabs << { :id => TAB_PERMISSIONS, :label => t('#account.tab_permissions', "Permissions"), :css_class => 'permissions', :href => :account_permissions_path } if user && self.grants_right?(user, nil, :manage_role_overrides)
if user && self.grants_right?(user, nil, :manage_outcomes)
tabs << { :id => TAB_OUTCOMES, :label => t('#account.tab_outcomes', "Outcomes"), :css_class => 'outcomes', :href => :account_outcomes_path }
tabs << { :id => TAB_RUBRICS, :label => t('#account.tab_rubrics', "Rubrics"), :css_class => 'rubrics', :href => :account_rubrics_path }
end
tabs << { :id => TAB_GRADING_STANDARDS, :label => t('#account.tab_grading_standards', "Grading Schemes"), :css_class => 'grading_standards', :href => :account_grading_standards_path } if user && self.grants_right?(user, nil, :manage_grades)
tabs << { :id => TAB_QUESTION_BANKS, :label => t('#account.tab_question_banks', "Question Banks"), :css_class => 'question_banks', :href => :account_question_banks_path } if user && self.grants_right?(user, nil, :manage_grades)
tabs << { :id => TAB_SUB_ACCOUNTS, :label => t('#account.tab_sub_accounts', "Sub-Accounts"), :css_class => 'sub_accounts', :href => :account_sub_accounts_path } if manage_settings
tabs << { :id => TAB_FACULTY_JOURNAL, :label => t('#account.tab_faculty_journal', "Faculty Journal"), :css_class => 'faculty_journal', :href => :account_user_notes_path} if self.enable_user_notes && user && self.grants_right?(user, nil, :manage_user_notes)
tabs << { :id => TAB_TERMS, :label => t('#account.tab_terms', "Terms"), :css_class => 'terms', :href => :account_terms_path } if self.root_account? && manage_settings
tabs << { :id => TAB_AUTHENTICATION, :label => t('#account.tab_authentication', "Authentication"), :css_class => 'authentication', :href => :account_account_authorization_configs_path } if self.root_account? && manage_settings
tabs << { :id => TAB_SIS_IMPORT, :label => t('#account.tab_sis_import', "SIS Import"), :css_class => 'sis_import', :href => :account_sis_import_path } if self.root_account? && self.allow_sis_import && user && self.grants_right?(user, nil, :manage_sis)
end
basic lti navigation links By properly configuring external tools (see /spec/models/course_spec/rb:898 for examples) they can be added as left-side navigation links to a course, an account, or to the user profile section of Canvas. testing notes: - you have to manually set options on the external tool: - for user navigation the tool needs to be created on the root account with the following settings: {:user_navigation => {:url => <url>, :text => <tab label>} } (there are also some optional language options you can set using the :labels attribute) - for account navigation it's the same - for course navigation it's the same, except with :course_navigation there's also some additional options: :visibility => <value> // public, members, admins :default => <value> // disabled, enabled test plan: - configure a user navigation tool at the root account level, make sure it shows up in the user's profile section - configure a course navigation tool at the account level, make sure it shows up in the course's navigation - configure a course navigation tool at the course level, make sure it shows up in the course's navigation - make sure :default => 'disabled' course navigation tools don't appear by default in the navigation, but can be enabled on the course settings page - make sure :visibility => 'members' only shows up for course members - make sure :visibility => 'admins' only shows up for course admins - configure an account navigation tool at the account level, make sure it shows up in the account's navigation, and any sub-account's navigation Change-Id: I977da3c6b89a9e32b4cff4c2b6b221f8162782ff Reviewed-on: https://gerrit.instructure.com/5427 Reviewed-by: Brian Whitmer <brian@instructure.com> Tested-by: Hudson <hudson@instructure.com>
2011-08-18 13:49:01 +08:00
tabs += external_tool_tabs(opts)
tabs << { :id => TAB_SETTINGS, :label => t('#account.tab_settings', "Settings"), :css_class => 'settings', :href => :account_settings_path }
2011-02-01 09:57:29 +08:00
tabs
end
def is_a_context?
true
end
def help_links
Canvas::Help.default_links + (settings[:custom_help_links] || [])
end
2011-02-01 09:57:29 +08:00
def self.allowable_services
{
:google_docs => {
:name => "Google Docs",
:description => "",
:expose_to_ui => (GoogleDocs.config ? :service : false)
},
:google_docs_previews => {
:name => "Google Docs Previews",
:description => "",
:expose_to_ui => :service
2011-02-01 09:57:29 +08:00
},
:facebook => {
:name => "Facebook",
:description => "",
:expose_to_ui => (Facebook.config ? :service : false)
2011-02-01 09:57:29 +08:00
},
:skype => {
:name => "Skype",
:description => "",
:expose_to_ui => :service
2011-02-01 09:57:29 +08:00
},
:linked_in => {
:name => "LinkedIn",
:description => "",
:expose_to_ui => (LinkedIn.config ? :service : false)
2011-02-01 09:57:29 +08:00
},
:twitter => {
:name => "Twitter",
:description => "",
:expose_to_ui => (Twitter.config ? :service : false)
2011-02-01 09:57:29 +08:00
},
:delicious => {
:name => "Delicious",
:description => "",
:expose_to_ui => :service
2011-02-01 09:57:29 +08:00
},
:diigo => {
:name => "Diigo",
:description => "",
:expose_to_ui => :service
2011-02-01 09:57:29 +08:00
},
# TODO: move avatars to :settings hash, it makes more sense there
# In the meantime, we leave it as a service but expose it in the
# "Features" (settings) portion of the account admin UI
2011-02-01 09:57:29 +08:00
:avatars => {
:name => "User Avatars",
:description => "",
:default => false,
:expose_to_ui => :setting
2011-02-01 09:57:29 +08:00
}
}.merge(@plugin_services || {}).freeze
2011-02-01 09:57:29 +08:00
end
def self.register_service(service_name, info_hash)
@plugin_services ||= {}
@plugin_services[service_name.to_sym] = info_hash.freeze
end
2011-02-01 09:57:29 +08:00
def self.default_allowable_services
self.allowable_services.reject {|s, info| info[:default] == false }
end
def set_service_availability(service, enable)
service = service.to_sym
raise "Invalid Service" unless Account.allowable_services[service]
allowed_service_names = (self.allowed_services || "").split(",").compact
if allowed_service_names.count > 0 and not [ '+', '-' ].member?(allowed_service_names[0][0,1])
# This account has a hard-coded list of services, so handle accordingly
allowed_service_names.reject! { |flag| flag.match("^[+-]?#{service}$") }
allowed_service_names << service if enable
else
allowed_service_names.reject! { |flag| flag.match("^[+-]?#{service}$") }
if enable
# only enable if it is not enabled by default
allowed_service_names << "+#{service}" unless Account.default_allowable_services[service]
else
# only disable if it is not enabled by default
allowed_service_names << "-#{service}" if Account.default_allowable_services[service]
end
end
@allowed_services_hash = nil
self.allowed_services = allowed_service_names.empty? ? nil : allowed_service_names.join(",")
end
def enable_service(service)
set_service_availability(service, true)
end
def disable_service(service)
set_service_availability(service, false)
end
2011-02-01 09:57:29 +08:00
def allowed_services_hash
return @allowed_services_hash if @allowed_services_hash
account_allowed_services = Account.default_allowable_services
if self.allowed_services
allowed_service_names = self.allowed_services.split(",").compact
if allowed_service_names.count > 0
unless [ '+', '-' ].member?(allowed_service_names[0][0,1])
# This account has a hard-coded list of services, so we clear out the defaults
account_allowed_services = { }
end
allowed_service_names.each do |service_switch|
if service_switch =~ /\A([+-]?)(.*)\z/
flag = $1
service_name = $2.to_sym
if flag == '-'
account_allowed_services.delete(service_name)
else
account_allowed_services[service_name] = Account.allowable_services[service_name]
end
end
end
end
end
@allowed_services_hash = account_allowed_services
end
# if expose_as is nil, all services exposed in the ui are returned
# if it's :service or :setting, then only services set to be exposed as that type are returned
def self.services_exposed_to_ui_hash(expose_as = nil)
if expose_as
self.allowable_services.reject { |key, setting| setting[:expose_to_ui] != expose_as }
else
self.allowable_services.reject { |key, setting| !setting[:expose_to_ui] }
end
end
2011-02-01 09:57:29 +08:00
def service_enabled?(service)
service = service.to_sym
case service
when :none
self.allowed_services_hash.empty?
else
self.allowed_services_hash.has_key?(service)
end
end
def self.all_accounts_for(context)
if context.respond_to?(:account)
context.account.account_chain
elsif context.respond_to?(:parent_account)
context.account_chain
else
[]
end
end
2011-02-01 09:57:29 +08:00
def self.serialization_excludes; [:uuid]; end
# This could be much faster if we implement a SQL tree for the account tree
# structure.
def find_child(child_id)
child_id = child_id.to_i
child_ids = self.class.connection.select_values("SELECT id FROM accounts WHERE parent_account_id = #{self.id}").map(&:to_i)
until child_ids.empty?
if child_ids.include?(child_id)
return self.class.find(child_id)
end
child_ids = self.class.connection.select_values("SELECT id FROM accounts WHERE parent_account_id IN (#{child_ids.join(",")})").map(&:to_i)
end
return false
end
def manually_created_courses_account
return self.root_account.manually_created_courses_account unless self.root_account?
display_name = t('#account.manually_created_courses', "Manually-Created Courses")
acct = manually_created_courses_account_from_settings
if acct.blank?
transaction do
lock!
acct = manually_created_courses_account_from_settings
acct ||= self.sub_accounts.find_by_name(display_name) # for backwards compatibility
acct ||= self.sub_accounts.create!(:name => display_name)
if acct.id != self.settings[:manually_created_courses_account_id]
self.settings[:manually_created_courses_account_id] = acct.id
self.save!
end
end
end
acct
end
def manually_created_courses_account_from_settings
acct_id = self.settings[:manually_created_courses_account_id]
acct = self.sub_accounts.find_by_id(acct_id) if acct_id.present?
acct = nil if acct.present? && acct.root_account_id != self.id
acct
end
private :manually_created_courses_account_from_settings
def trusted_account_ids
return [] if !root_account? || self == Account.site_admin
[ Account.site_admin.id ]
end
introduce preferred user list search mode closes #7059, #7411 preferred is a compromise between open registration and closed registration. if a single user is found, that user is found without introducing any temporary user to allow user disambiguation. if no users are found, or multiple users are found, it's the same behavior as open registration. as part of this, the "only search existing users" checkbox has been removed. instead, preferred searching will be used in the following cases: * adding an admin, and open registration is enabled * adding an admin, open registration is disabled, and the user has permission to create new users * adding a user to a course, open registration is enabled, and delegated authentication is in use * adding a user to a course, open registration is disabled, and the user has permission to create new users the notable case that preferred searching is not used, and the user would want to use search existing users is open registration enabled, and adding a user to a course. in this case, a temporary user will be created instead of sending the invitation to the existing user. however, the end user experience is identical (invitation sent to e-mail, invitation visible in the same locations in the UI), with the small exception that the end user gets the opportunity to create a separate account if desired (but not preferred). test plan: * go through each of the cases above for a user that doesn't exist yet, a single matching user exists, or multiple users exist. it should behave as described above Change-Id: Idbc7fe23bfc7430b4cd25f7981f5dc08b9e4ffda Reviewed-on: https://gerrit.instructure.com/8966 Tested-by: Hudson <hudson@instructure.com> Reviewed-by: Brian Palmer <brianp@instructure.com>
2012-02-24 06:24:48 +08:00
def user_list_search_mode_for(user)
return :preferred if self.root_account.open_registration?
return :preferred if self.root_account.grants_right?(user, :manage_user_logins)
:closed
end
named_scope :root_accounts, :conditions => {:root_account_id => nil}
named_scope :processing_sis_batch, :conditions => ['accounts.current_sis_batch_id IS NOT NULL'], :order => :updated_at
2011-02-01 09:57:29 +08:00
named_scope :name_like, lambda { |name|
{ :conditions => wildcard('accounts.name', name) }
2011-02-01 09:57:29 +08:00
}
named_scope :active, :conditions => ['accounts.workflow_state != ?', 'deleted']
2011-02-01 09:57:29 +08:00
named_scope :limit, lambda {|limit|
{:limit => limit}
}
def canvas_network_enabled?
false
end
def import_from_migration(data, params, migration)
LearningOutcome.process_migration(data, migration)
migration.progress=100
migration.workflow_state = :imported
migration.save
end
2011-02-01 09:57:29 +08:00
end