<commit_before># frozen_string_literal: true

#
# Copyright (C) 2011 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#

require "atom"

class Account < ActiveRecord::Base
  include Context
  include OutcomeImportContext
  include Pronouns
  include SearchTermHelper

  INSTANCE_GUID_SUFFIX = "canvas-lms"
  # a list of columns necessary for validation and save callbacks to work on a slim object
  BASIC_COLUMNS_FOR_CALLBACKS = %i[id parent_account_id root_account_id name workflow_state settings].freeze

  include Workflow
  include BrandConfigHelpers
  belongs_to :root_account, class_name: "Account"
  belongs_to :parent_account, class_name: "Account"

  has_many :courses
  has_many :favorites, inverse_of: :root_account
  has_many :all_courses, class_name: "Course", foreign_key: "root_account_id"
  has_one :terms_of_service, dependent: :destroy
  has_one :terms_of_service_content, dependent: :destroy
  has_many :group_categories, -> { where(deleted_at: nil) }, as: :context, inverse_of: :context
  has_many :all_group_categories, class_name: "GroupCategory", foreign_key: "root_account_id", inverse_of: :root_account
  has_many :groups, as: :context, inverse_of: :context
  has_many :all_groups, class_name: "Group", foreign_key: "root_account_id", inverse_of: :root_account
  has_many :all_group_memberships, source: "group_memberships", through: :all_groups
  has_many :enrollment_terms, foreign_key: "root_account_id"
  has_many :active_enrollment_terms, -> { where("enrollment_terms.workflow_state<>'deleted'") }, class_name: "EnrollmentTerm", foreign_key: "root_account_id"
  has_many :grading_period_groups, inverse_of: :root_account, dependent: :destroy
  has_many :grading_periods, through: :grading_period_groups
  has_many :enrollments, -> { where("enrollments.type<>'StudentViewEnrollment'") }, foreign_key: "root_account_id"
  has_many :all_enrollments, class_name: "Enrollment", foreign_key: "root_account_id"
  has_many :sub_accounts, -> { where("workflow_state<>'deleted'") }, class_name: "Account", foreign_key: "parent_account_id"
  has_many :all_accounts, -> { order(:name) }, class_name: "Account", foreign_key: "root_account_id"
  has_many :account_users, dependent: :destroy
  has_many :active_account_users, -> { active }, class_name: "AccountUser"
  has_many :course_sections, foreign_key: "root_account_id"
  has_many :sis_batches
  has_many :abstract_courses, class_name: "AbstractCourse"
  has_many :root_abstract_courses, class_name: "AbstractCourse", foreign_key: "root_account_id"
  has_many :all_users, -> { distinct }, through: :user_account_associations, source: :user
  has_many :users, through: :active_account_users
  has_many :user_past_lti_ids, as: :context, inverse_of: :context
  has_many :pseudonyms, -> { preload(:user) }, inverse_of: :account
  has_many :role_overrides, as: :context, inverse_of: :context
  has_many :course_account_associations
  has_many :child_courses, -> { where(course_account_associations: { depth: 0 }) }, through: :course_account_associations, source: :course
  has_many :attachments, as: :context, inverse_of: :context, dependent: :destroy
  has_many :active_assignments, -> { where("assignments.workflow_state<>'deleted'") }, as: :context, inverse_of: :context, class_name: "Assignment"
  has_many :folders, -> { order("folders.name") }, as: :context, inverse_of: :context, dependent: :destroy
  has_many :active_folders, -> { where("folder.workflow_state<>'deleted'").order("folders.name") }, class_name: "Folder", as: :context, inverse_of: :context
  has_many :developer_keys
  has_many :developer_key_account_bindings, inverse_of: :account, dependent: :destroy
  has_many :authentication_providers,
           -> { ordered },
           inverse_of: :account,
           extend: AuthenticationProvider::FindWithType

  has_many :account_reports, inverse_of: :account
  has_many :grading_standards, -> { where("workflow_state<>'deleted'") }, as: :context, inverse_of: :context
  has_many :assessment_question_banks, -> { preload(:assessment_questions, :assessment_question_bank_users) }, as: :context, inverse_of: :context
  has_many :assessment_questions, through: :assessment_question_banks
  has_many :roles
  has_many :all_roles, class_name: "Role", foreign_key: "root_account_id"
  has_many :progresses, as: :context, inverse_of: :context
  has_many :content_migrations, as: :context, inverse_of: :context
  has_many :sis_batch_errors, foreign_key: :root_account_id, inverse_of: :root_account
  has_many :canvadocs_annotation_contexts
  has_one :outcome_proficiency, -> { preload(:outcome_proficiency_ratings) }, as: :context, inverse_of: :context, dependent: :destroy
  has_one :outcome_calculation_method, as: :context, inverse_of: :context, dependent: :destroy

  has_many :auditor_authentication_records,
           class_name: "Auditors::ActiveRecord::AuthenticationRecord",
           dependent: :destroy,
           inverse_of: :account
  has_many :auditor_course_records,
           class_name: "Auditors::ActiveRecord::CourseRecord",
           dependent: :destroy,
           inverse_of: :account
  has_many :auditor_grade_change_records,
           class_name: "Auditors::ActiveRecord::GradeChangeRecord",
           dependent: :destroy,
           inverse_of: :account
  has_many :auditor_root_grade_change_records,
           foreign_key: "root_account_id",
           class_name: "Auditors::ActiveRecord::GradeChangeRecord",
           dependent: :destroy,
           inverse_of: :root_account
  has_many :auditor_feature_flag_records,
           foreign_key: "root_account_id",
           class_name: "Auditors::ActiveRecord::FeatureFlagRecord",
           dependent: :destroy,
           inverse_of: :root_account
  has_many :auditor_pseudonym_records,
           foreign_key: "root_account_id",
           class_name: "Auditors::ActiveRecord::PseudonymRecord",
           inverse_of: :root_account
  has_many :lti_resource_links,
           as: :context,
           inverse_of: :context,
           class_name: "Lti::ResourceLink",
           dependent: :destroy
  belongs_to :course_template, class_name: "Course", inverse_of: :templated_accounts

  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 do |c|
      sql << "context_type = ? AND context_id = ?"
      conds += [c.class.to_s, c.id]
    end
    conds.unshift(sql.join(" OR "))
    AssessmentQuestionBank.where(conds)
  end

  include LearningOutcomeContext
  include RubricContext

  has_many :context_external_tools, -> { order(:name) }, as: :context, inverse_of: :context, dependent: :destroy
  has_many :error_reports
  has_many :announcements, class_name: "AccountNotification"
  has_many :alerts, -> { preload(:criteria) }, as: :context, inverse_of: :context
  has_many :user_account_associations
  has_many :report_snapshots
  has_many :external_integration_keys, as: :context, inverse_of: :context, dependent: :destroy
  has_many :shared_brand_configs
  belongs_to :brand_config, foreign_key: "brand_config_md5"
  has_many :blackout_dates, as: :context, inverse_of: :context

  before_validation :verify_unique_sis_source_id
  before_save :ensure_defaults
  before_create :enable_sis_imports, if: :root_account?
  after_save :update_account_associations_if_changed
  after_save :check_downstream_caches

  before_save :setup_cache_invalidation
  after_save :invalidate_caches_if_changed
  after_update :clear_special_account_cache_if_special

  after_update :clear_cached_short_name, if: :saved_change_to_name?

  after_create :create_default_objects

  serialize :settings, Hash
  include TimeZoneHelper

  time_zone_attribute :default_time_zone, default: "America/Denver"
  def default_time_zone
    if read_attribute(:default_time_zone) || root_account?
      super
    else
      root_account.default_time_zone
    end
  end
  alias_method :time_zone, :default_time_zone

  validates_locale :default_locale, allow_nil: true
  validates :name, length: { maximum: maximum_string_length, allow_blank: true }
  validate :account_chain_loop, if: :parent_account_id_changed?
  validate :validate_auth_discovery_url
  validates :workflow_state, presence: true
  validate :no_active_courses, if: ->(a) { a.workflow_state_changed? && !a.active? }
  validate :no_active_sub_accounts, if: ->(a) { a.workflow_state_changed? && !a.active? }
  validate :validate_help_links, if: ->(a) { a.settings_changed? }
  validate :validate_course_template, if: ->(a) { a.has_attribute?(:course_template_id) && a.course_template_id_changed? }

  include StickySisFields
  are_sis_sticky :name, :parent_account_id

  include FeatureFlags
  def feature_flag_cache
    MultiCache.cache
  end

  def self.recursive_default_locale_for_id(account_id)
    local_id, shard = Shard.local_id_for(account_id)
    (shard || Shard.current).activate do
      obj = Account.new(id: local_id) # someday i should figure out a better way to avoid instantiating an object instead of tricking cache register
      Rails.cache.fetch_with_batched_keys("default_locale_for_id", batch_object: obj, batched_keys: [:account_chain, :default_locale]) do
        # couldn't find the cache so now we actually need to find the account
        acc = Account.find(local_id)
        acc.default_locale || (acc.parent_account_id && recursive_default_locale_for_id(acc.parent_account_id))
      end
    end
  end

  def default_locale
    result = read_attribute(:default_locale)
    result = nil unless I18n.locale_available?(result)
    result
  end

  def resolved_outcome_proficiency
    cache_key = ["outcome_proficiency", cache_key(:resolved_outcome_proficiency), cache_key(:account_chain)].cache_key
    Rails.cache.fetch(cache_key) do
      if outcome_proficiency&.active?
        outcome_proficiency
      elsif parent_account
        parent_account.resolved_outcome_proficiency
      elsif feature_enabled?(:account_level_mastery_scales)
        OutcomeProficiency.find_or_create_default!(self)
      end
    end
  end

  def resolved_outcome_calculation_method
    cache_key = ["outcome_calculation_method", cache_key(:resolved_outcome_calculation_method), cache_key(:account_chain)].cache_key
    Rails.cache.fetch(cache_key) do
      if outcome_calculation_method&.active?
        outcome_calculation_method
      elsif parent_account
        parent_account.resolved_outcome_calculation_method
      elsif feature_enabled?(:account_level_mastery_scales)
        OutcomeCalculationMethod.find_or_create_default!(self)
      end
    end
  end

  def allow_student_anonymous_discussion_topics
    false
  end

  include ::Account::Settings
  include ::Csp::AccountHelper

  # these settings either are or could be easily added to
  # the account settings page
  add_setting :sis_app_token, root_only: true
  add_setting :sis_app_url, root_only: true
  add_setting :sis_name, root_only: true
  add_setting :sis_syncing, boolean: true, default: false, inheritable: true
  add_setting :sis_default_grade_export, boolean: true, default: false, inheritable: true
  add_setting :include_integration_ids_in_gradebook_exports, boolean: true, default: false, root_only: true
  add_setting :sis_require_assignment_due_date, boolean: true, default: false, inheritable: true
  add_setting :sis_assignment_name_length, boolean: true, default: false, inheritable: true
  add_setting :sis_assignment_name_length_input, inheritable: true

  add_setting :global_includes, root_only: true, boolean: true, default: false
  add_setting :sub_account_includes, boolean: true, default: false

  # Microsoft Sync Account Settings
  add_setting :microsoft_sync_enabled, root_only: true, boolean: true, default: false
  add_setting :microsoft_sync_tenant, root_only: true
  add_setting :microsoft_sync_login_attribute, root_only: true
  add_setting :microsoft_sync_login_attribute_suffix, root_only: true
  add_setting :microsoft_sync_remote_attribute, root_only: true

  # Help link settings
  add_setting :custom_help_links, root_only: true
  add_setting :new_custom_help_links, root_only: true
  add_setting :help_link_icon, root_only: true
  add_setting :help_link_name, root_only: true
  add_setting :support_url, root_only: true

  add_setting :prevent_course_renaming_by_teachers, boolean: true, root_only: true
  add_setting :prevent_course_availability_editing_by_teachers, boolean: true, root_only: true
  add_setting :login_handle_name, root_only: true
  add_setting :change_password_url, root_only: true
  add_setting :unknown_user_url, root_only: true
  add_setting :fft_registration_url, root_only: true

  add_setting :restrict_student_future_view, boolean: true, default: false, inheritable: true
  add_setting :restrict_student_future_listing, boolean: true, default: false, inheritable: true
  add_setting :restrict_student_past_view, boolean: true, default: false, inheritable: true

  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 :teachers_can_create_courses_anywhere, boolean: true, root_only: true, default: true
  add_setting :students_can_create_courses_anywhere, boolean: true, root_only: true, default: true

  add_setting :restrict_quiz_questions, boolean: true, root_only: true, default: false
  add_setting :allow_sending_scores_in_emails, boolean: true, root_only: true
  add_setting :can_add_pronouns, boolean: true, root_only: true, default: false
  add_setting :can_change_pronouns, boolean: true, root_only: true, default: true
  add_setting :enable_sis_export_pronouns, boolean: true, root_only: true, default: true
  add_setting :pronouns, 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, default: true
  add_setting :open_registration, boolean: true, root_only: true
  add_setting :show_scheduler, boolean: true, root_only: true, default: false
  add_setting :enable_profiles, boolean: true, root_only: true, default: false
  add_setting :enable_turnitin, boolean: true, default: false
  add_setting :mfa_settings, root_only: true
  add_setting :mobile_qr_login_is_enabled, boolean: true, root_only: true, default: true
  add_setting :admins_can_change_passwords, boolean: true, root_only: true, default: false
  add_setting :admins_can_view_notifications, boolean: true, root_only: true, default: false
  add_setting :canvadocs_prefer_office_online, boolean: true, root_only: true, default: false
  add_setting :outgoing_email_default_name, root_only: true
  add_setting :external_notification_warning, boolean: true, root_only: true, default: false
  # Terms of Use and Privacy Policy settings for the root account
  add_setting :terms_changed_at, root_only: true
  add_setting :account_terms_required, root_only: true, boolean: true, default: true
  # 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
  add_setting :large_course_rosters, boolean: true, root_only: true, default: false
  add_setting :edit_institution_email, boolean: true, root_only: true, default: true
  add_setting :js_kaltura_uploader, boolean: true, root_only: true, default: false
  add_setting :google_docs_domain, root_only: true
  add_setting :dashboard_url, root_only: true
  add_setting :product_name, root_only: true
  add_setting :author_email_in_notifications, boolean: true, root_only: true, default: false
  add_setting :include_students_in_global_survey, boolean: true, root_only: true, default: false
  add_setting :trusted_referers, root_only: true
  add_setting :app_center_access_token
  add_setting :enable_offline_web_export, boolean: true, default: false, inheritable: true
  add_setting :disable_rce_media_uploads, boolean: true, default: false, inheritable: true
  add_setting :allow_gradebook_show_first_last_names, boolean: true, default: false

  add_setting :strict_sis_check, boolean: true, root_only: true, default: false
  add_setting :lock_all_announcements, default: false, boolean: true, inheritable: true

  add_setting :enable_gravatar, boolean: true, root_only: true, default: true

  # For setting the default dashboard (e.g. Student Planner/List View, Activity Stream, Dashboard Cards)
  add_setting :default_dashboard_view, inheritable: true

  add_setting :require_confirmed_email, boolean: true, root_only: true, default: false

  add_setting :enable_course_catalog, boolean: true, root_only: true, default: false
  add_setting :usage_rights_required, boolean: true, default: false, inheritable: true
  add_setting :limit_parent_app_web_access, boolean: true, default: false, root_only: true
  add_setting :kill_joy, boolean: true, default: false, root_only: true
  add_setting :smart_alerts_threshold, default: 36, root_only: true

  add_setting :disable_post_to_sis_when_grading_period_closed, boolean: true, root_only: true, default: false

  # privacy settings for root accounts
  add_setting :enable_fullstory, boolean: true, root_only: true, default: true

  add_setting :rce_favorite_tool_ids, inheritable: true

  add_setting :enable_as_k5_account, boolean: true, default: false, inheritable: true
  # Allow accounts with strict data residency requirements to turn off mobile
  # push notifications which may be routed through US datacenters by Google/Apple
  add_setting :enable_push_notifications, boolean: true, root_only: true, default: true
  add_setting :allow_last_page_on_course_users, boolean: true, root_only: true, default: false
  add_setting :allow_last_page_on_account_courses, boolean: true, root_only: true, default: false
  add_setting :allow_last_page_on_users, boolean: true, root_only: true, default: false
  add_setting :emoji_deny_list, root_only: true

  add_setting :default_due_time, inheritable: true
  add_setting :conditional_release, default: false, boolean: true, inheritable: true

  def settings=(hash)
    if hash.is_a?(Hash) || hash.is_a?(ActionController::Parameters)
      hash.each do |key, val|
        key = key.to_sym
        if account_settings_options && (opts = account_settings_options[key])
          if (opts[:root_only] && !root_account?) || (opts[:condition] && !send("#{opts[:condition]}?".to_sym))
            settings.delete key
          elsif opts[:hash]
            new_hash = {}
            if val.is_a?(Hash) || val.is_a?(ActionController::Parameters)
              val.each do |inner_key, inner_val|
                inner_key = inner_key.to_sym
                next unless opts[:values].include?(inner_key)

                new_hash[inner_key] = if opts[:inheritable] && (inner_key == :locked || (inner_key == :value && opts[:boolean]))
                                        Canvas::Plugin.value_to_boolean(inner_val)
                                      else
                                        inner_val.to_s.presence
                                      end
              end
            end
            settings[key] = new_hash.empty? ? nil : new_hash
          elsif opts[:boolean]
            settings[key] = Canvas::Plugin.value_to_boolean(val)
          else
            settings[key] = val.to_s.presence
          end
        end
      end
    end
    # prune nil or "" hash values to save space in the DB.
    settings.reject! { |_, value| value.nil? || value == { value: nil } || value == { value: nil, locked: false } }
    settings
  end

  def product_name
    settings[:product_name] || t("#product_name", "Canvas")
  end

  def usage_rights_required?
    usage_rights_required[:value]
  end

  def allow_global_includes?
    if root_account?
      global_includes?
    else
      root_account.try(:sub_account_includes?) && root_account.try(:allow_global_includes?)
    end
  end

  def pronouns
    return [] unless settings[:can_add_pronouns]

    settings[:pronouns]&.map { |p| translate_pronouns(p) } || Pronouns.default_pronouns
  end

  def pronouns=(pronouns)
    settings[:pronouns] = pronouns&.map { |p| untranslate_pronouns(p) }&.reject(&:blank?)
  end

  def mfa_settings
    settings[:mfa_settings].try(:to_sym) || :disabled
  end

  def non_canvas_auth_configured?
    authentication_providers.active.where("auth_type<>'canvas'").exists?
  end

  def canvas_authentication_provider
    @canvas_ap ||= authentication_providers.active.where(auth_type: "canvas").first
  end

  def canvas_authentication?
    !!canvas_authentication_provider
  end

  def enable_canvas_authentication
    return unless root_account?
    # for migrations creating a new db
    return unless Account.connection.data_source_exists?("authentication_providers")
    return if authentication_providers.active.where(auth_type: "canvas").exists?

    authentication_providers.create!(auth_type: "canvas")
  end

  def enable_offline_web_export?
    enable_offline_web_export[:value]
  end

  def disable_rce_media_uploads?
    disable_rce_media_uploads[:value]
  end

  def enable_as_k5_account?
    enable_as_k5_account[:value]
  end

  def enable_as_k5_account!
    settings[:enable_as_k5_account] = { value: true }
    save!
  end

  def conditional_release?
    conditional_release[:value]
  end

  def open_registration?
    !!settings[:open_registration] && canvas_authentication?
  end

  def self_registration?
    canvas_authentication_provider.try(:jit_provisioning?)
  end

  def self_registration_type
    canvas_authentication_provider.try(:self_registration)
  end

  def self_registration_captcha?
    canvas_authentication_provider.try(:enable_captcha)
  end

  def self_registration_allowed_for?(type)
    return false unless self_registration?
    return false if self_registration_type != "all" && type != self_registration_type

    true
  end

  def enable_self_registration
    canvas_authentication_provider.update_attribute(:self_registration, true)
  end

  def terms_required?
    terms = TermsOfService.ensure_terms_for_account(root_account)
    !(terms.terms_type == "no_terms" || terms.passive)
  end

  def require_acceptance_of_terms?(user)
    return false unless terms_required?
    return true if user.nil? || user.new_record?

    soc2_start_date = Setting.get("SOC2_start_date", Time.new(2015, 5, 16, 0, 0, 0).utc).to_datetime
    return false if user.created_at < soc2_start_date

    terms_changed_at = root_account.terms_of_service.terms_of_service_content&.terms_updated_at || settings[:terms_changed_at]
    last_accepted = user.preferences[:accepted_terms]
    return false if last_accepted && (terms_changed_at.nil? || last_accepted > terms_changed_at)

    true
  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

  def enable_sis_imports
    self.allow_sis_import = true
  end

  def ensure_defaults
    name&.delete!("\r")
    self.uuid ||= CanvasSlug.generate_securish_uuid if has_attribute?(:uuid)
    self.lti_guid ||= "#{self.uuid}:#{INSTANCE_GUID_SUFFIX}" if has_attribute?(:lti_guid)
    self.root_account_id ||= parent_account.root_account_id if parent_account && !parent_account.root_account?
    self.root_account_id ||= parent_account_id
    self.parent_account_id ||= self.root_account_id unless root_account?
    unless root_account_id
      Account.ensure_dummy_root_account
      self.root_account_id = 0
    end
    true
  end

  def verify_unique_sis_source_id
    return true unless has_attribute?(:sis_source_id)
    return true unless sis_source_id
    return true if !root_account_id_changed? && !sis_source_id_changed?

    if root_account?
      errors.add(:sis_source_id, t("#account.root_account_cant_have_sis_id", "SIS IDs cannot be set on root accounts"))
      throw :abort
    end

    scope = root_account.all_accounts.where(sis_source_id: sis_source_id)
    scope = scope.where("id<>?", self) unless new_record?

    return true unless scope.exists?

    errors.add(:sis_source_id, t("#account.sis_id_in_use", "SIS ID \"%{sis_id}\" is already in use", sis_id: sis_source_id))
    throw :abort
  end

  def update_account_associations_if_changed
    # if the account structure changed, but this is _not_ a new object
    if (saved_change_to_parent_account_id? || saved_change_to_root_account_id?) &&
       !saved_change_to_id?
      shard.activate do
        delay_if_production.update_account_associations
      end
    end
  end

  def check_downstream_caches
    # dummy account has no downstream
    return if dummy?
    return if ActiveRecord::Base.in_migration

    keys_to_clear = []
    keys_to_clear << :account_chain if saved_change_to_parent_account_id? || saved_change_to_root_account_id?
    if saved_change_to_brand_config_md5? || (@old_settings && @old_settings[:sub_account_includes] != settings[:sub_account_includes])
      keys_to_clear << :brand_config
    end
    keys_to_clear << :default_locale if saved_change_to_default_locale?
    if keys_to_clear.any?
      shard.activate do
        self.class.connection.after_transaction_commit do
          delay_if_production(singleton: "Account#clear_downstream_caches/#{global_id}")
            .clear_downstream_caches(*keys_to_clear, xlog_location: self.class.current_xlog_location)
        end
      end
    end
  end

  def clear_downstream_caches(*key_types, xlog_location: nil, is_retry: false)
    shard.activate do
      if xlog_location
        timeout = Setting.get("account_cache_clear_replication_timeout", "60").to_i.seconds
        unless self.class.wait_for_replication(start: xlog_location, timeout: timeout)
          delay(run_at: Time.now + timeout, singleton: "Account#clear_downstream_caches/#{global_id}")
            .clear_downstream_caches(*keys_to_clear, xlog_location: xlog_location, is_retry: true)
          # we still clear, but only the first time; after that we just keep waiting
          return if is_retry
        end
      end

      Account.clear_cache_keys([id] + Account.sub_account_ids_recursive(id), *key_types)
    end
  end

  def equella_settings
    endpoint = settings[:equella_endpoint] || equella_endpoint
    if endpoint.blank?
      nil
    else
      OpenObject.new({
                       endpoint: endpoint,
                       default_action: settings[:equella_action] || "selectOrAdd",
                       teaser: settings[:equella_teaser]
                     })
    end
  end<commit_msg>Fix downstream cache requeueing<commit_after># frozen_string_literal: true

#
# Copyright (C) 2011 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#

require "atom"

class Account < ActiveRecord::Base
  include Context
  include OutcomeImportContext
  include Pronouns
  include SearchTermHelper

  INSTANCE_GUID_SUFFIX = "canvas-lms"
  # a list of columns necessary for validation and save callbacks to work on a slim object
  BASIC_COLUMNS_FOR_CALLBACKS = %i[id parent_account_id root_account_id name workflow_state settings].freeze

  include Workflow
  include BrandConfigHelpers
  belongs_to :root_account, class_name: "Account"
  belongs_to :parent_account, class_name: "Account"

  has_many :courses
  has_many :favorites, inverse_of: :root_account
  has_many :all_courses, class_name: "Course", foreign_key: "root_account_id"
  has_one :terms_of_service, dependent: :destroy
  has_one :terms_of_service_content, dependent: :destroy
  has_many :group_categories, -> { where(deleted_at: nil) }, as: :context, inverse_of: :context
  has_many :all_group_categories, class_name: "GroupCategory", foreign_key: "root_account_id", inverse_of: :root_account
  has_many :groups, as: :context, inverse_of: :context
  has_many :all_groups, class_name: "Group", foreign_key: "root_account_id", inverse_of: :root_account
  has_many :all_group_memberships, source: "group_memberships", through: :all_groups
  has_many :enrollment_terms, foreign_key: "root_account_id"
  has_many :active_enrollment_terms, -> { where("enrollment_terms.workflow_state<>'deleted'") }, class_name: "EnrollmentTerm", foreign_key: "root_account_id"
  has_many :grading_period_groups, inverse_of: :root_account, dependent: :destroy
  has_many :grading_periods, through: :grading_period_groups
  has_many :enrollments, -> { where("enrollments.type<>'StudentViewEnrollment'") }, foreign_key: "root_account_id"
  has_many :all_enrollments, class_name: "Enrollment", foreign_key: "root_account_id"
  has_many :sub_accounts, -> { where("workflow_state<>'deleted'") }, class_name: "Account", foreign_key: "parent_account_id"
  has_many :all_accounts, -> { order(:name) }, class_name: "Account", foreign_key: "root_account_id"
  has_many :account_users, dependent: :destroy
  has_many :active_account_users, -> { active }, class_name: "AccountUser"
  has_many :course_sections, foreign_key: "root_account_id"
  has_many :sis_batches
  has_many :abstract_courses, class_name: "AbstractCourse"
  has_many :root_abstract_courses, class_name: "AbstractCourse", foreign_key: "root_account_id"
  has_many :all_users, -> { distinct }, through: :user_account_associations, source: :user
  has_many :users, through: :active_account_users
  has_many :user_past_lti_ids, as: :context, inverse_of: :context
  has_many :pseudonyms, -> { preload(:user) }, inverse_of: :account
  has_many :role_overrides, as: :context, inverse_of: :context
  has_many :course_account_associations
  has_many :child_courses, -> { where(course_account_associations: { depth: 0 }) }, through: :course_account_associations, source: :course
  has_many :attachments, as: :context, inverse_of: :context, dependent: :destroy
  has_many :active_assignments, -> { where("assignments.workflow_state<>'deleted'") }, as: :context, inverse_of: :context, class_name: "Assignment"
  has_many :folders, -> { order("folders.name") }, as: :context, inverse_of: :context, dependent: :destroy
  has_many :active_folders, -> { where("folder.workflow_state<>'deleted'").order("folders.name") }, class_name: "Folder", as: :context, inverse_of: :context
  has_many :developer_keys
  has_many :developer_key_account_bindings, inverse_of: :account, dependent: :destroy
  has_many :authentication_providers,
           -> { ordered },
           inverse_of: :account,
           extend: AuthenticationProvider::FindWithType

  has_many :account_reports, inverse_of: :account
  has_many :grading_standards, -> { where("workflow_state<>'deleted'") }, as: :context, inverse_of: :context
  has_many :assessment_question_banks, -> { preload(:assessment_questions, :assessment_question_bank_users) }, as: :context, inverse_of: :context
  has_many :assessment_questions, through: :assessment_question_banks
  has_many :roles
  has_many :all_roles, class_name: "Role", foreign_key: "root_account_id"
  has_many :progresses, as: :context, inverse_of: :context
  has_many :content_migrations, as: :context, inverse_of: :context
  has_many :sis_batch_errors, foreign_key: :root_account_id, inverse_of: :root_account
  has_many :canvadocs_annotation_contexts
  has_one :outcome_proficiency, -> { preload(:outcome_proficiency_ratings) }, as: :context, inverse_of: :context, dependent: :destroy
  has_one :outcome_calculation_method, as: :context, inverse_of: :context, dependent: :destroy

  has_many :auditor_authentication_records,
           class_name: "Auditors::ActiveRecord::AuthenticationRecord",
           dependent: :destroy,
           inverse_of: :account
  has_many :auditor_course_records,
           class_name: "Auditors::ActiveRecord::CourseRecord",
           dependent: :destroy,
           inverse_of: :account
  has_many :auditor_grade_change_records,
           class_name: "Auditors::ActiveRecord::GradeChangeRecord",
           dependent: :destroy,
           inverse_of: :account
  has_many :auditor_root_grade_change_records,
           foreign_key: "root_account_id",
           class_name: "Auditors::ActiveRecord::GradeChangeRecord",
           dependent: :destroy,
           inverse_of: :root_account
  has_many :auditor_feature_flag_records,
           foreign_key: "root_account_id",
           class_name: "Auditors::ActiveRecord::FeatureFlagRecord",
           dependent: :destroy,
           inverse_of: :root_account
  has_many :auditor_pseudonym_records,
           foreign_key: "root_account_id",
           class_name: "Auditors::ActiveRecord::PseudonymRecord",
           inverse_of: :root_account
  has_many :lti_resource_links,
           as: :context,
           inverse_of: :context,
           class_name: "Lti::ResourceLink",
           dependent: :destroy
  belongs_to :course_template, class_name: "Course", inverse_of: :templated_accounts

  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 do |c|
      sql << "context_type = ? AND context_id = ?"
      conds += [c.class.to_s, c.id]
    end
    conds.unshift(sql.join(" OR "))
    AssessmentQuestionBank.where(conds)
  end

  include LearningOutcomeContext
  include RubricContext

  has_many :context_external_tools, -> { order(:name) }, as: :context, inverse_of: :context, dependent: :destroy
  has_many :error_reports
  has_many :announcements, class_name: "AccountNotification"
  has_many :alerts, -> { preload(:criteria) }, as: :context, inverse_of: :context
  has_many :user_account_associations
  has_many :report_snapshots
  has_many :external_integration_keys, as: :context, inverse_of: :context, dependent: :destroy
  has_many :shared_brand_configs
  belongs_to :brand_config, foreign_key: "brand_config_md5"
  has_many :blackout_dates, as: :context, inverse_of: :context

  before_validation :verify_unique_sis_source_id
  before_save :ensure_defaults
  before_create :enable_sis_imports, if: :root_account?
  after_save :update_account_associations_if_changed
  after_save :check_downstream_caches

  before_save :setup_cache_invalidation
  after_save :invalidate_caches_if_changed
  after_update :clear_special_account_cache_if_special

  after_update :clear_cached_short_name, if: :saved_change_to_name?

  after_create :create_default_objects

  serialize :settings, Hash
  include TimeZoneHelper

  time_zone_attribute :default_time_zone, default: "America/Denver"
  def default_time_zone
    if read_attribute(:default_time_zone) || root_account?
      super
    else
      root_account.default_time_zone
    end
  end
  alias_method :time_zone, :default_time_zone

  validates_locale :default_locale, allow_nil: true
  validates :name, length: { maximum: maximum_string_length, allow_blank: true }
  validate :account_chain_loop, if: :parent_account_id_changed?
  validate :validate_auth_discovery_url
  validates :workflow_state, presence: true
  validate :no_active_courses, if: ->(a) { a.workflow_state_changed? && !a.active? }
  validate :no_active_sub_accounts, if: ->(a) { a.workflow_state_changed? && !a.active? }
  validate :validate_help_links, if: ->(a) { a.settings_changed? }
  validate :validate_course_template, if: ->(a) { a.has_attribute?(:course_template_id) && a.course_template_id_changed? }

  include StickySisFields
  are_sis_sticky :name, :parent_account_id

  include FeatureFlags
  def feature_flag_cache
    MultiCache.cache
  end

  def self.recursive_default_locale_for_id(account_id)
    local_id, shard = Shard.local_id_for(account_id)
    (shard || Shard.current).activate do
      obj = Account.new(id: local_id) # someday i should figure out a better way to avoid instantiating an object instead of tricking cache register
      Rails.cache.fetch_with_batched_keys("default_locale_for_id", batch_object: obj, batched_keys: [:account_chain, :default_locale]) do
        # couldn't find the cache so now we actually need to find the account
        acc = Account.find(local_id)
        acc.default_locale || (acc.parent_account_id && recursive_default_locale_for_id(acc.parent_account_id))
      end
    end
  end

  def default_locale
    result = read_attribute(:default_locale)
    result = nil unless I18n.locale_available?(result)
    result
  end

  def resolved_outcome_proficiency
    cache_key = ["outcome_proficiency", cache_key(:resolved_outcome_proficiency), cache_key(:account_chain)].cache_key
    Rails.cache.fetch(cache_key) do
      if outcome_proficiency&.active?
        outcome_proficiency
      elsif parent_account
        parent_account.resolved_outcome_proficiency
      elsif feature_enabled?(:account_level_mastery_scales)
        OutcomeProficiency.find_or_create_default!(self)
      end
    end
  end

  def resolved_outcome_calculation_method
    cache_key = ["outcome_calculation_method", cache_key(:resolved_outcome_calculation_method), cache_key(:account_chain)].cache_key
    Rails.cache.fetch(cache_key) do
      if outcome_calculation_method&.active?
        outcome_calculation_method
      elsif parent_account
        parent_account.resolved_outcome_calculation_method
      elsif feature_enabled?(:account_level_mastery_scales)
        OutcomeCalculationMethod.find_or_create_default!(self)
      end
    end
  end

  def allow_student_anonymous_discussion_topics
    false
  end

  include ::Account::Settings
  include ::Csp::AccountHelper

  # these settings either are or could be easily added to
  # the account settings page
  add_setting :sis_app_token, root_only: true
  add_setting :sis_app_url, root_only: true
  add_setting :sis_name, root_only: true
  add_setting :sis_syncing, boolean: true, default: false, inheritable: true
  add_setting :sis_default_grade_export, boolean: true, default: false, inheritable: true
  add_setting :include_integration_ids_in_gradebook_exports, boolean: true, default: false, root_only: true
  add_setting :sis_require_assignment_due_date, boolean: true, default: false, inheritable: true
  add_setting :sis_assignment_name_length, boolean: true, default: false, inheritable: true
  add_setting :sis_assignment_name_length_input, inheritable: true

  add_setting :global_includes, root_only: true, boolean: true, default: false
  add_setting :sub_account_includes, boolean: true, default: false

  # Microsoft Sync Account Settings
  add_setting :microsoft_sync_enabled, root_only: true, boolean: true, default: false
  add_setting :microsoft_sync_tenant, root_only: true
  add_setting :microsoft_sync_login_attribute, root_only: true
  add_setting :microsoft_sync_login_attribute_suffix, root_only: true
  add_setting :microsoft_sync_remote_attribute, root_only: true

  # Help link settings
  add_setting :custom_help_links, root_only: true
  add_setting :new_custom_help_links, root_only: true
  add_setting :help_link_icon, root_only: true
  add_setting :help_link_name, root_only: true
  add_setting :support_url, root_only: true

  add_setting :prevent_course_renaming_by_teachers, boolean: true, root_only: true
  add_setting :prevent_course_availability_editing_by_teachers, boolean: true, root_only: true
  add_setting :login_handle_name, root_only: true
  add_setting :change_password_url, root_only: true
  add_setting :unknown_user_url, root_only: true
  add_setting :fft_registration_url, root_only: true

  add_setting :restrict_student_future_view, boolean: true, default: false, inheritable: true
  add_setting :restrict_student_future_listing, boolean: true, default: false, inheritable: true
  add_setting :restrict_student_past_view, boolean: true, default: false, inheritable: true

  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 :teachers_can_create_courses_anywhere, boolean: true, root_only: true, default: true
  add_setting :students_can_create_courses_anywhere, boolean: true, root_only: true, default: true

  add_setting :restrict_quiz_questions, boolean: true, root_only: true, default: false
  add_setting :allow_sending_scores_in_emails, boolean: true, root_only: true
  add_setting :can_add_pronouns, boolean: true, root_only: true, default: false
  add_setting :can_change_pronouns, boolean: true, root_only: true, default: true
  add_setting :enable_sis_export_pronouns, boolean: true, root_only: true, default: true
  add_setting :pronouns, 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, default: true
  add_setting :open_registration, boolean: true, root_only: true
  add_setting :show_scheduler, boolean: true, root_only: true, default: false
  add_setting :enable_profiles, boolean: true, root_only: true, default: false
  add_setting :enable_turnitin, boolean: true, default: false
  add_setting :mfa_settings, root_only: true
  add_setting :mobile_qr_login_is_enabled, boolean: true, root_only: true, default: true
  add_setting :admins_can_change_passwords, boolean: true, root_only: true, default: false
  add_setting :admins_can_view_notifications, boolean: true, root_only: true, default: false
  add_setting :canvadocs_prefer_office_online, boolean: true, root_only: true, default: false
  add_setting :outgoing_email_default_name, root_only: true
  add_setting :external_notification_warning, boolean: true, root_only: true, default: false
  # Terms of Use and Privacy Policy settings for the root account
  add_setting :terms_changed_at, root_only: true
  add_setting :account_terms_required, root_only: true, boolean: true, default: true
  # 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
  add_setting :large_course_rosters, boolean: true, root_only: true, default: false
  add_setting :edit_institution_email, boolean: true, root_only: true, default: true
  add_setting :js_kaltura_uploader, boolean: true, root_only: true, default: false
  add_setting :google_docs_domain, root_only: true
  add_setting :dashboard_url, root_only: true
  add_setting :product_name, root_only: true
  add_setting :author_email_in_notifications, boolean: true, root_only: true, default: false
  add_setting :include_students_in_global_survey, boolean: true, root_only: true, default: false
  add_setting :trusted_referers, root_only: true
  add_setting :app_center_access_token
  add_setting :enable_offline_web_export, boolean: true, default: false, inheritable: true
  add_setting :disable_rce_media_uploads, boolean: true, default: false, inheritable: true
  add_setting :allow_gradebook_show_first_last_names, boolean: true, default: false

  add_setting :strict_sis_check, boolean: true, root_only: true, default: false
  add_setting :lock_all_announcements, default: false, boolean: true, inheritable: true

  add_setting :enable_gravatar, boolean: true, root_only: true, default: true

  # For setting the default dashboard (e.g. Student Planner/List View, Activity Stream, Dashboard Cards)
  add_setting :default_dashboard_view, inheritable: true

  add_setting :require_confirmed_email, boolean: true, root_only: true, default: false

  add_setting :enable_course_catalog, boolean: true, root_only: true, default: false
  add_setting :usage_rights_required, boolean: true, default: false, inheritable: true
  add_setting :limit_parent_app_web_access, boolean: true, default: false, root_only: true
  add_setting :kill_joy, boolean: true, default: false, root_only: true
  add_setting :smart_alerts_threshold, default: 36, root_only: true

  add_setting :disable_post_to_sis_when_grading_period_closed, boolean: true, root_only: true, default: false

  # privacy settings for root accounts
  add_setting :enable_fullstory, boolean: true, root_only: true, default: true

  add_setting :rce_favorite_tool_ids, inheritable: true

  add_setting :enable_as_k5_account, boolean: true, default: false, inheritable: true
  # Allow accounts with strict data residency requirements to turn off mobile
  # push notifications which may be routed through US datacenters by Google/Apple
  add_setting :enable_push_notifications, boolean: true, root_only: true, default: true
  add_setting :allow_last_page_on_course_users, boolean: true, root_only: true, default: false
  add_setting :allow_last_page_on_account_courses, boolean: true, root_only: true, default: false
  add_setting :allow_last_page_on_users, boolean: true, root_only: true, default: false
  add_setting :emoji_deny_list, root_only: true

  add_setting :default_due_time, inheritable: true
  add_setting :conditional_release, default: false, boolean: true, inheritable: true

  def settings=(hash)
    if hash.is_a?(Hash) || hash.is_a?(ActionController::Parameters)
      hash.each do |key, val|
        key = key.to_sym
        if account_settings_options && (opts = account_settings_options[key])
          if (opts[:root_only] && !root_account?) || (opts[:condition] && !send("#{opts[:condition]}?".to_sym))
            settings.delete key
          elsif opts[:hash]
            new_hash = {}
            if val.is_a?(Hash) || val.is_a?(ActionController::Parameters)
              val.each do |inner_key, inner_val|
                inner_key = inner_key.to_sym
                next unless opts[:values].include?(inner_key)

                new_hash[inner_key] = if opts[:inheritable] && (inner_key == :locked || (inner_key == :value && opts[:boolean]))
                                        Canvas::Plugin.value_to_boolean(inner_val)
                                      else
                                        inner_val.to_s.presence
                                      end
              end
            end
            settings[key] = new_hash.empty? ? nil : new_hash
          elsif opts[:boolean]
            settings[key] = Canvas::Plugin.value_to_boolean(val)
          else
            settings[key] = val.to_s.presence
          end
        end
      end
    end
    # prune nil or "" hash values to save space in the DB.
    settings.reject! { |_, value| value.nil? || value == { value: nil } || value == { value: nil, locked: false } }
    settings
  end

  def product_name
    settings[:product_name] || t("#product_name", "Canvas")
  end

  def usage_rights_required?
    usage_rights_required[:value]
  end

  def allow_global_includes?
    if root_account?
      global_includes?
    else
      root_account.try(:sub_account_includes?) && root_account.try(:allow_global_includes?)
    end
  end

  def pronouns
    return [] unless settings[:can_add_pronouns]

    settings[:pronouns]&.map { |p| translate_pronouns(p) } || Pronouns.default_pronouns
  end

  def pronouns=(pronouns)
    settings[:pronouns] = pronouns&.map { |p| untranslate_pronouns(p) }&.reject(&:blank?)
  end

  def mfa_settings
    settings[:mfa_settings].try(:to_sym) || :disabled
  end

  def non_canvas_auth_configured?
    authentication_providers.active.where("auth_type<>'canvas'").exists?
  end

  def canvas_authentication_provider
    @canvas_ap ||= authentication_providers.active.where(auth_type: "canvas").first
  end

  def canvas_authentication?
    !!canvas_authentication_provider
  end

  def enable_canvas_authentication
    return unless root_account?
    # for migrations creating a new db
    return unless Account.connection.data_source_exists?("authentication_providers")
    return if authentication_providers.active.where(auth_type: "canvas").exists?

    authentication_providers.create!(auth_type: "canvas")
  end

  def enable_offline_web_export?
    enable_offline_web_export[:value]
  end

  def disable_rce_media_uploads?
    disable_rce_media_uploads[:value]
  end

  def enable_as_k5_account?
    enable_as_k5_account[:value]
  end

  def enable_as_k5_account!
    settings[:enable_as_k5_account] = { value: true }
    save!
  end

  def conditional_release?
    conditional_release[:value]
  end

  def open_registration?
    !!settings[:open_registration] && canvas_authentication?
  end

  def self_registration?
    canvas_authentication_provider.try(:jit_provisioning?)
  end

  def self_registration_type
    canvas_authentication_provider.try(:self_registration)
  end

  def self_registration_captcha?
    canvas_authentication_provider.try(:enable_captcha)
  end

  def self_registration_allowed_for?(type)
    return false unless self_registration?
    return false if self_registration_type != "all" && type != self_registration_type

    true
  end

  def enable_self_registration
    canvas_authentication_provider.update_attribute(:self_registration, true)
  end

  def terms_required?
    terms = TermsOfService.ensure_terms_for_account(root_account)
    !(terms.terms_type == "no_terms" || terms.passive)
  end

  def require_acceptance_of_terms?(user)
    return false unless terms_required?
    return true if user.nil? || user.new_record?

    soc2_start_date = Setting.get("SOC2_start_date", Time.new(2015, 5, 16, 0, 0, 0).utc).to_datetime
    return false if user.created_at < soc2_start_date

    terms_changed_at = root_account.terms_of_service.terms_of_service_content&.terms_updated_at || settings[:terms_changed_at]
    last_accepted = user.preferences[:accepted_terms]
    return false if last_accepted && (terms_changed_at.nil? || last_accepted > terms_changed_at)

    true
  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

  def enable_sis_imports
    self.allow_sis_import = true
  end

  def ensure_defaults
    name&.delete!("\r")
    self.uuid ||= CanvasSlug.generate_securish_uuid if has_attribute?(:uuid)
    self.lti_guid ||= "#{self.uuid}:#{INSTANCE_GUID_SUFFIX}" if has_attribute?(:lti_guid)
    self.root_account_id ||= parent_account.root_account_id if parent_account && !parent_account.root_account?
    self.root_account_id ||= parent_account_id
    self.parent_account_id ||= self.root_account_id unless root_account?
    unless root_account_id
      Account.ensure_dummy_root_account
      self.root_account_id = 0
    end
    true
  end

  def verify_unique_sis_source_id
    return true unless has_attribute?(:sis_source_id)
    return true unless sis_source_id
    return true if !root_account_id_changed? && !sis_source_id_changed?

    if root_account?
      errors.add(:sis_source_id, t("#account.root_account_cant_have_sis_id", "SIS IDs cannot be set on root accounts"))
      throw :abort
    end

    scope = root_account.all_accounts.where(sis_source_id: sis_source_id)
    scope = scope.where("id<>?", self) unless new_record?

    return true unless scope.exists?

    errors.add(:sis_source_id, t("#account.sis_id_in_use", "SIS ID \"%{sis_id}\" is already in use", sis_id: sis_source_id))
    throw :abort
  end

  def update_account_associations_if_changed
    # if the account structure changed, but this is _not_ a new object
    if (saved_change_to_parent_account_id? || saved_change_to_root_account_id?) &&
       !saved_change_to_id?
      shard.activate do
        delay_if_production.update_account_associations
      end
    end
  end

  def check_downstream_caches
    # dummy account has no downstream
    return if dummy?
    return if ActiveRecord::Base.in_migration

    keys_to_clear = []
    keys_to_clear << :account_chain if saved_change_to_parent_account_id? || saved_change_to_root_account_id?
    if saved_change_to_brand_config_md5? || (@old_settings && @old_settings[:sub_account_includes] != settings[:sub_account_includes])
      keys_to_clear << :brand_config
    end
    keys_to_clear << :default_locale if saved_change_to_default_locale?
    if keys_to_clear.any?
      shard.activate do
        self.class.connection.after_transaction_commit do
          delay_if_production(singleton: "Account#clear_downstream_caches/#{global_id}:#{keys_to_clear.join("/")}")
            .clear_downstream_caches(*keys_to_clear, xlog_location: self.class.current_xlog_location)
        end
      end
    end
  end

  def clear_downstream_caches(*keys_to_clear, xlog_location: nil, is_retry: false)
    shard.activate do
      if xlog_location
        timeout = Setting.get("account_cache_clear_replication_timeout", "60").to_i.seconds
        unless self.class.wait_for_replication(start: xlog_location, timeout: timeout)
          delay(run_at: Time.now + timeout, singleton: "Account#clear_downstream_caches/#{global_id}:#{keys_to_clear.join("/")}")
            .clear_downstream_caches(*keys_to_clear, xlog_location: xlog_location, is_retry: true)
          # we still clear, but only the first time; after that we just keep waiting
          return if is_retry
        end
      end

      Account.clear_cache_keys([id] + Account.sub_account_ids_recursive(id), *keys_to_clear)
    end
  end

  def equella_settings
    endpoint = settings[:equella_endpoint] || equella_endpoint
    if endpoint.blank?
      nil
    else
      OpenObject.new({
                       endpoint: endpoint,
                       default_action: settings[:equella_action] || "selectOrAdd",
                       teaser: settings[:equella_teaser]
                     })
    end
  end
{'fraction_non_alphanumeric': 0.0849944924137441, 'fraction_numerical': 0.0012791813239526704, 'mean_word_length': 4.680964876867178, 'pattern_counts': {'":': 0, '<': 47, '<?xml version=': 0, '>': 67, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 8, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2318615', 'n_tokens_mistral': 17853, 'n_tokens_neox': 17442, 'n_words': 4218}