panda_pal 5.11.0 → 5.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +12 -0
- data/app/models/panda_pal/organization.rb +3 -39
- data/app/models/panda_pal/organization_concerns/multi_database_sharding.rb +22 -0
- data/app/models/panda_pal/organization_concerns/tenant_handling.rb +54 -0
- data/config/initializers/apartment.rb +202 -3
- data/lib/panda_pal/engine.rb +0 -2
- data/lib/panda_pal/spec_helper.rb +82 -0
- data/lib/panda_pal/version.rb +1 -1
- data/panda_pal.gemspec +3 -3
- data/spec/core/apartment_multidb_spec.rb +48 -0
- data/spec/dummy/config/database.yml +6 -2
- data/spec/dummy/config/environments/test.rb +5 -0
- data/spec/dummy/db/test2_schema.rb +18 -0
- data/spec/models/panda_pal/api_call_spec.rb +1 -1
- data/spec/models/panda_pal/organization/settings_validation_spec.rb +4 -0
- data/spec/models/panda_pal/organization/task_scheduling_spec.rb +0 -0
- data/spec/rails_helper.rb +4 -0
- data/spec/spec_helper.rb +0 -1
- metadata +52 -45
- /data/spec/dummy/{bin → app/bin}/bundle +0 -0
- /data/spec/dummy/{bin → app/bin}/rails +0 -0
- /data/spec/dummy/{bin → app/bin}/rake +0 -0
- /data/spec/dummy/{bin → app/bin}/setup +0 -0
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: ab899936461dc59208ae6afef28264feda7a75927798e158ca1ee8381b2ea463
         | 
| 4 | 
            +
              data.tar.gz: 20424ac6bef9bede8af9e58e8a1393267b0966f96cf56dd8ae8ed0e5b486420a
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 85041955aabe8255fcf0a4e3cbcfce3c55c690e0d66d74ac511d6b4d70825d796a62a18fe3ac3e0b5dc66e9ddc865c5b45f672940d92c15a1cc38a7ac350ad91
         | 
| 7 | 
            +
              data.tar.gz: bcbe2513a4b4916af671f5a913036e0154dd474d510351f6325a2f264d03c1dac10adc4c48bc060c40faafdc798268664c48e2c32943dd4ed3d0dcda076b7ff3
         | 
    
        data/README.md
    CHANGED
    
    | @@ -179,6 +179,18 @@ Rails.application.config.middleware.use 'Apartment::Elevators::Generic', lambda | |
| 179 179 | 
             
            It is also possible to switch tenants by implementing `around_action :switch_tenant` in the controller.
         | 
| 180 180 | 
             
            However, it should be noted that calling `switch_tenant` must be used in an `around_action` hook (or given a block), and is only intended to be used for LTI launches, and not subsequent requests.
         | 
| 181 181 |  | 
| 182 | 
            +
            ### Sharding
         | 
| 183 | 
            +
            PandaPal 5.12 added support for multi-DB sharding. If you wish to use this, add a `shard` column to the `PandaPal::Organization` model. The implementation should be fairly transparent and should behave as normal PandaPal/Apartment - the only difference is that tenants can be created on different DB shards.
         | 
| 184 | 
            +
             | 
| 185 | 
            +
            The list of available shards is computed from the Environment variables. On startup, the application looks for variables with the following formats:
         | 
| 186 | 
            +
            - `SHARD_DB_XYZ_URL`
         | 
| 187 | 
            +
            - `HEROKU_POSTGRESQL_XYZ_URL`
         | 
| 188 | 
            +
            (where `XYZ` is the shard identifier)
         | 
| 189 | 
            +
             | 
| 190 | 
            +
            When an `Organization` is created, it is assigned to a random shard (or if no shards were discovered, it is placed in the default database as normal). Alternatively, a specific shard can be specified: `PandaPal::Organization.new(shard: "shard_a")`. An `Organization` can be added to the primary shard with `shard: "default"`
         | 
| 191 | 
            +
             | 
| 192 | 
            +
            The `PandaPal::Organization` record is still created in the `public` schema of the default database, as per usual.
         | 
| 193 | 
            +
             | 
| 182 194 | 
             
            ### Rake tasks and jobs
         | 
| 183 195 | 
             
            **Delayed Job Support has been removed. This allows each project to assess it's need to handle background jobs.**
         | 
| 184 196 |  | 
| @@ -41,6 +41,9 @@ module PandaPal | |
| 41 41 | 
             
                include OrganizationConcerns::OrganizationBuilder if $stdin&.tty?
         | 
| 42 42 | 
             
                include OrganizationConcerns::TaskScheduling if defined?(Sidekiq.schedule)
         | 
| 43 43 |  | 
| 44 | 
            +
                include OrganizationConcerns::TenantHandling
         | 
| 45 | 
            +
                include OrganizationConcerns::MultiDatabaseSharding if (column_names rescue []).include?('shard')
         | 
| 46 | 
            +
             | 
| 44 47 | 
             
                attr_encrypted :settings, marshal: true, key: :encryption_key, marshaler: SettingsMarshaler
         | 
| 45 48 | 
             
                before_save {|a| a.settings = a.settings} # this is a hacky work-around to a bug where attr_encrypted is not saving settings in place
         | 
| 46 49 |  | 
| @@ -56,9 +59,6 @@ module PandaPal | |
| 56 59 | 
             
                validates :canvas_account_id, presence: true
         | 
| 57 60 | 
             
                validates :salesforce_id, presence: true, uniqueness: true
         | 
| 58 61 |  | 
| 59 | 
            -
                after_create :create_schema
         | 
| 60 | 
            -
                after_commit :destroy_schema, on: :destroy
         | 
| 61 | 
            -
             | 
| 62 62 | 
             
                define_setting("lti", {
         | 
| 63 63 | 
             
                  type: 'Hash',
         | 
| 64 64 | 
             
                  required: false,
         | 
| @@ -71,10 +71,6 @@ module PandaPal | |
| 71 71 | 
             
                  end
         | 
| 72 72 | 
             
                end
         | 
| 73 73 |  | 
| 74 | 
            -
                before_validation on: [:update] do
         | 
| 75 | 
            -
                  errors.add(:name, 'should not be changed after creation') if name_changed?
         | 
| 76 | 
            -
                end
         | 
| 77 | 
            -
             | 
| 78 74 | 
             
                def encryption_key
         | 
| 79 75 | 
             
                  # production environment might not have loaded secret_key_base yet.
         | 
| 80 76 | 
             
                  # In that case, just read it from env.
         | 
| @@ -85,18 +81,6 @@ module PandaPal | |
| 85 81 | 
             
                  end
         | 
| 86 82 | 
             
                end
         | 
| 87 83 |  | 
| 88 | 
            -
                def switch_tenant(&block)
         | 
| 89 | 
            -
                  if block_given?
         | 
| 90 | 
            -
                    Apartment::Tenant.switch(name, &block)
         | 
| 91 | 
            -
                  else
         | 
| 92 | 
            -
                    Apartment::Tenant.switch!(name)
         | 
| 93 | 
            -
                  end
         | 
| 94 | 
            -
                end
         | 
| 95 | 
            -
             | 
| 96 | 
            -
                def self.current
         | 
| 97 | 
            -
                  find_by_name(Apartment::Tenant.current)
         | 
| 98 | 
            -
                end
         | 
| 99 | 
            -
             | 
| 100 84 | 
             
                def create_api(logic, expiration: nil, uses: nil, host: nil)
         | 
| 101 85 | 
             
                  switch_tenant do
         | 
| 102 86 | 
             
                    logic = "current_organization.#{logic}" if logic.is_a?(Symbol)
         | 
| @@ -109,16 +93,6 @@ module PandaPal | |
| 109 93 | 
             
                  end
         | 
| 110 94 | 
             
                end
         | 
| 111 95 |  | 
| 112 | 
            -
                def rename!(new_name)
         | 
| 113 | 
            -
                  do_switch = Apartment::Tenant.current == name
         | 
| 114 | 
            -
                  ActiveRecord::Base.connection.execute(
         | 
| 115 | 
            -
                    "ALTER SCHEMA \"#{name}\" RENAME TO \"#{new_name}\";"
         | 
| 116 | 
            -
                  )
         | 
| 117 | 
            -
                  self.class.where(id: id).update_all(name: new_name)
         | 
| 118 | 
            -
                  reload
         | 
| 119 | 
            -
                  switch_tenant if do_switch
         | 
| 120 | 
            -
                end
         | 
| 121 | 
            -
             | 
| 122 96 | 
             
                if !PandaPal.lti_options[:platform].present? || PandaPal.lti_options[:platform].is_a?(String)
         | 
| 123 97 | 
             
                  CONST_PLATFORM_TYPE = Platform.resolve_platform_class(nil) rescue nil
         | 
| 124 98 | 
             
                else
         | 
| @@ -192,16 +166,6 @@ module PandaPal | |
| 192 166 | 
             
                  CONST_PLATFORM_TYPE
         | 
| 193 167 | 
             
                end
         | 
| 194 168 |  | 
| 195 | 
            -
                private
         | 
| 196 | 
            -
             | 
| 197 | 
            -
                def create_schema
         | 
| 198 | 
            -
                  Apartment::Tenant.create name
         | 
| 199 | 
            -
                end
         | 
| 200 | 
            -
             | 
| 201 | 
            -
                def destroy_schema
         | 
| 202 | 
            -
                  Apartment::Tenant.drop name
         | 
| 203 | 
            -
                end
         | 
| 204 | 
            -
             | 
| 205 169 | 
             
                public
         | 
| 206 170 |  | 
| 207 171 | 
             
                PandaPal.resolved_extensions_for(self).each do |ext|
         | 
| @@ -0,0 +1,22 @@ | |
| 1 | 
            +
            module PandaPal
         | 
| 2 | 
            +
              module OrganizationConcerns
         | 
| 3 | 
            +
                module MultiDatabaseSharding
         | 
| 4 | 
            +
                  extend ActiveSupport::Concern
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                  included do
         | 
| 7 | 
            +
                    if column_names.include?('shard')
         | 
| 8 | 
            +
                      validates :shard, format: { with: /\A[a-z0-9_]+\z/i }, allow_blank: true
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                      before_validation on: [:update] do
         | 
| 11 | 
            +
                        errors.add(:shard, 'should not be changed after creation') if shard_changed?
         | 
| 12 | 
            +
                      end
         | 
| 13 | 
            +
                    end
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  def tenant_name
         | 
| 17 | 
            +
                    return "#{shard}:#{name}" if shard.present?
         | 
| 18 | 
            +
                    super
         | 
| 19 | 
            +
                  end
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
              end
         | 
| 22 | 
            +
            end
         | 
| @@ -0,0 +1,54 @@ | |
| 1 | 
            +
            module PandaPal
         | 
| 2 | 
            +
              module OrganizationConcerns
         | 
| 3 | 
            +
                module TenantHandling
         | 
| 4 | 
            +
                  extend ActiveSupport::Concern
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                  class_methods do
         | 
| 7 | 
            +
                    def current
         | 
| 8 | 
            +
                      find_by_name(Apartment::Tenant.current)
         | 
| 9 | 
            +
                    end
         | 
| 10 | 
            +
                  end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  included do
         | 
| 13 | 
            +
                    after_create :create_schema
         | 
| 14 | 
            +
                    after_commit :destroy_schema, on: :destroy
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                    before_validation on: [:update] do
         | 
| 17 | 
            +
                      errors.add(:name, 'should not be changed after creation') if name_changed?
         | 
| 18 | 
            +
                    end
         | 
| 19 | 
            +
                  end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                  def tenant_name
         | 
| 22 | 
            +
                    name
         | 
| 23 | 
            +
                  end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                  def switch_tenant(&block)
         | 
| 26 | 
            +
                    if block_given?
         | 
| 27 | 
            +
                      Apartment::Tenant.switch(tenant_name, &block)
         | 
| 28 | 
            +
                    else
         | 
| 29 | 
            +
                      Apartment::Tenant.switch!(tenant_name)
         | 
| 30 | 
            +
                    end
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                  def rename!(new_name)
         | 
| 34 | 
            +
                    do_switch = Apartment::Tenant.current == name
         | 
| 35 | 
            +
                    ActiveRecord::Base.connection.execute(
         | 
| 36 | 
            +
                      "ALTER SCHEMA \"#{name}\" RENAME TO \"#{new_name}\";"
         | 
| 37 | 
            +
                    )
         | 
| 38 | 
            +
                    self.class.where(id: id).update_all(name: new_name)
         | 
| 39 | 
            +
                    reload
         | 
| 40 | 
            +
                    switch_tenant if do_switch
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  private
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                  def create_schema
         | 
| 46 | 
            +
                    Apartment::Tenant.create tenant_name
         | 
| 47 | 
            +
                  end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  def destroy_schema
         | 
| 50 | 
            +
                    Apartment::Tenant.drop tenant_name
         | 
| 51 | 
            +
                  end
         | 
| 52 | 
            +
                end
         | 
| 53 | 
            +
              end
         | 
| 54 | 
            +
            end
         | 
| @@ -5,13 +5,212 @@ begin | |
| 5 5 | 
             
            rescue LoadError
         | 
| 6 6 | 
             
            end
         | 
| 7 7 |  | 
| 8 | 
            +
            require "apartment/adapters/postgresql_adapter"
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            module Apartment
         | 
| 11 | 
            +
              SHARD_PREFIXES = ["SHARD_DB", "HEROKU_POSTGRESQL"]
         | 
| 12 | 
            +
             | 
| 13 | 
            +
              def self.shard_configurations
         | 
| 14 | 
            +
                $shard_configurations ||= begin
         | 
| 15 | 
            +
                  shard_to_env = {}
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                  ENV.keys.each do |k|
         | 
| 18 | 
            +
                    m = /^(#{SHARD_PREFIXES.join("|")})_(\w+)_URL$/.match(k)
         | 
| 19 | 
            +
                    next unless m
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                    url = ENV[k]
         | 
| 22 | 
            +
                    shard_to_env[m[2].downcase] = ActiveRecord::Base.configurations.resolve(url).configuration_hash
         | 
| 23 | 
            +
                  end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                  shard_to_env.freeze unless Rails.env.test?
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                  shard_to_env
         | 
| 28 | 
            +
                end
         | 
| 29 | 
            +
              end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
              module Tenant
         | 
| 32 | 
            +
                self.singleton_class.send(:alias_method, :original_postgresql_adapter, :postgresql_adapter)
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                def self.postgresql_adapter(config)
         | 
| 35 | 
            +
                  if Apartment.with_multi_server_setup
         | 
| 36 | 
            +
                    adapter = Adapters::PostgresMultiDBSchemaAdapter
         | 
| 37 | 
            +
                    adapter.new(config)
         | 
| 38 | 
            +
                  else
         | 
| 39 | 
            +
                    original_postgresql_adapter(config)
         | 
| 40 | 
            +
                  end
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                def self.split_tenant(tenant)
         | 
| 44 | 
            +
                  bits = tenant.split(":", 2)
         | 
| 45 | 
            +
                  bits.unshift(nil) if bits.length == 1
         | 
| 46 | 
            +
                  bits[0] = "default" if bits[0].nil? || bits[0].empty?
         | 
| 47 | 
            +
                  bits
         | 
| 48 | 
            +
                end
         | 
| 49 | 
            +
              end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
              module Adapters
         | 
| 52 | 
            +
                class PostgresMultiDBSchemaAdapter < Apartment::Adapters::PostgresqlSchemaAdapter
         | 
| 53 | 
            +
                  def initialize(*args, **kwargs)
         | 
| 54 | 
            +
                    super
         | 
| 55 | 
            +
                    @excluded_model_set = Set.new(Apartment.excluded_models)
         | 
| 56 | 
            +
                  end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                  def process_excluded_model(excluded_model)
         | 
| 59 | 
            +
                    @excluded_model_set << excluded_model
         | 
| 60 | 
            +
                    super
         | 
| 61 | 
            +
                  end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                  def is_excluded_model?(model)
         | 
| 64 | 
            +
                    @excluded_model_set.include?(model.to_s)
         | 
| 65 | 
            +
                  end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                  def db_connection_config(tenant)
         | 
| 68 | 
            +
                    shard, schema = Tenant.split_tenant(tenant)
         | 
| 69 | 
            +
                    if shard == "default"
         | 
| 70 | 
            +
                      @config
         | 
| 71 | 
            +
                    else
         | 
| 72 | 
            +
                      Apartment.shard_configurations[shard.downcase]
         | 
| 73 | 
            +
                    end
         | 
| 74 | 
            +
                  end
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                  def drop_command(conn, tenant)
         | 
| 77 | 
            +
                    shard, schema = Tenant.split_tenant(tenant)
         | 
| 78 | 
            +
                    conn.execute(%(DROP SCHEMA "#{schema}" CASCADE))
         | 
| 79 | 
            +
                  end
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                  def tenant_exists?(tenant)
         | 
| 82 | 
            +
                    return true unless Apartment.tenant_presence_check
         | 
| 83 | 
            +
                    shard, schema = Tenant.split_tenant(tenant)
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                    Apartment.connection.schema_exists?(schema)
         | 
| 86 | 
            +
                  end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                  def create_tenant_command(conn, tenant)
         | 
| 89 | 
            +
                    shard, schema = Tenant.split_tenant(tenant)
         | 
| 90 | 
            +
                    # NOTE: This was causing some tests to fail because of the database strategy for rspec
         | 
| 91 | 
            +
                    if conn.open_transactions.positive?
         | 
| 92 | 
            +
                      conn.execute(%(CREATE SCHEMA "#{schema}")).inspect
         | 
| 93 | 
            +
                    else
         | 
| 94 | 
            +
                      schema = %(BEGIN;
         | 
| 95 | 
            +
                      CREATE SCHEMA "#{schema}";
         | 
| 96 | 
            +
                      COMMIT;)
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                      conn.execute(schema)
         | 
| 99 | 
            +
                    end
         | 
| 100 | 
            +
                  rescue *rescuable_exceptions => e
         | 
| 101 | 
            +
                    rollback_transaction(conn)
         | 
| 102 | 
            +
                    raise e
         | 
| 103 | 
            +
                  end
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                  def connect_to_new(tenant = nil)
         | 
| 106 | 
            +
                    return reset if tenant.nil?
         | 
| 107 | 
            +
             | 
| 108 | 
            +
                    current_tenant = @current
         | 
| 109 | 
            +
                    tenants_array = tenant.is_a?(Array) ? tenant.map(&:to_s) : tenant.to_s
         | 
| 110 | 
            +
                    tenant_schemas = map_to_schemas(Array(tenants_array))
         | 
| 111 | 
            +
                    query_cache_enabled = ActiveRecord::Base.connection.query_cache_enabled
         | 
| 112 | 
            +
             | 
| 113 | 
            +
                    @current = tenants_array
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                    raise ActiveRecord::StatementInvalid, "PandaPal/Apartment Mutli-DB support does not currently support DB roles" if ActiveRecord::Base.current_role != ActiveRecord::Base.default_role
         | 
| 116 | 
            +
             | 
| 117 | 
            +
                    unless ActiveRecord::Base.connected?
         | 
| 118 | 
            +
                      Apartment.establish_connection multi_tenantify(tenant, false)
         | 
| 119 | 
            +
                      Apartment.connection.verify!
         | 
| 120 | 
            +
                    end
         | 
| 121 | 
            +
             | 
| 122 | 
            +
                    Apartment.connection.enable_query_cache! if query_cache_enabled
         | 
| 123 | 
            +
             | 
| 124 | 
            +
                    raise ActiveRecord::StatementInvalid, "Could not find schema for tenant #{tenant} (#{tenant_schemas.inspect})" unless schema_exists?(tenant_schemas)
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                    Apartment.connection.schema_search_path = full_search_path
         | 
| 127 | 
            +
                  rescue *rescuable_exceptions => e
         | 
| 128 | 
            +
                    @current = current_tenant
         | 
| 129 | 
            +
                    raise_schema_connect_to_new(tenant, e)
         | 
| 130 | 
            +
                  end
         | 
| 131 | 
            +
             | 
| 132 | 
            +
                  protected
         | 
| 133 | 
            +
             | 
| 134 | 
            +
                  def persistent_schemas
         | 
| 135 | 
            +
                    map_to_schemas(super)
         | 
| 136 | 
            +
                  end
         | 
| 137 | 
            +
             | 
| 138 | 
            +
                  def map_to_schemas(tenants)
         | 
| 139 | 
            +
                    all_shard = nil
         | 
| 140 | 
            +
                    tenants.map do |schema|
         | 
| 141 | 
            +
                      shard, schema = Tenant.split_tenant(schema)
         | 
| 142 | 
            +
                      all_shard ||= shard
         | 
| 143 | 
            +
                      raise "Cannot mix shards in persistent_schemas" if shard != all_shard
         | 
| 144 | 
            +
                      schema
         | 
| 145 | 
            +
                    end
         | 
| 146 | 
            +
                  end
         | 
| 147 | 
            +
                end
         | 
| 148 | 
            +
              end
         | 
| 149 | 
            +
             | 
| 150 | 
            +
              module CorePatches
         | 
| 151 | 
            +
                module ActiveRecord
         | 
| 152 | 
            +
                  module FutureResult
         | 
| 153 | 
            +
                    # called in the original thread that knows the tenant
         | 
| 154 | 
            +
                    def initialize(_pool, *_args, **_kwargs)
         | 
| 155 | 
            +
                      @tenant = Apartment::Tenant.current
         | 
| 156 | 
            +
                      super
         | 
| 157 | 
            +
                    end
         | 
| 158 | 
            +
             | 
| 159 | 
            +
                    # called in the new thread with a connection that needs switching
         | 
| 160 | 
            +
                    def exec_query(_conn, *_args, **_kwargs)
         | 
| 161 | 
            +
                      Apartment::Tenant.switch!(@tenant) unless Apartment::Tenant.current == @tenant
         | 
| 162 | 
            +
                      super
         | 
| 163 | 
            +
                    end
         | 
| 164 | 
            +
                  end
         | 
| 165 | 
            +
             | 
| 166 | 
            +
                  module Base
         | 
| 167 | 
            +
                    extend ActiveSupport::Concern
         | 
| 168 | 
            +
             | 
| 169 | 
            +
                    included do
         | 
| 170 | 
            +
                      self.singleton_class.send(:alias_method, :pre_apartment_current_shard, :current_shard)
         | 
| 171 | 
            +
             | 
| 172 | 
            +
                      def self.current_shard
         | 
| 173 | 
            +
                        # This implementation is definitely a hack, but it should be fairly compatible. If you need to leverage
         | 
| 174 | 
            +
                        #   Rails' sharding natively - you should just need to add models to the excluded_models list in Apartment
         | 
| 175 | 
            +
                        #   to effectively disable this patch for that model.
         | 
| 176 | 
            +
                        if (adapter = Thread.current[:apartment_adapter]) && adapter.is_a?(Apartment::Adapters::PostgresMultiDBSchemaAdapter) && !adapter.is_excluded_model?(self)
         | 
| 177 | 
            +
                          shard, schema = Apartment::Tenant.split_tenant(adapter.current)
         | 
| 178 | 
            +
                          return "apt:#{shard}" unless shard == "default"
         | 
| 179 | 
            +
                        end
         | 
| 180 | 
            +
             | 
| 181 | 
            +
                        pre_apartment_current_shard
         | 
| 182 | 
            +
                      end
         | 
| 183 | 
            +
                    end
         | 
| 184 | 
            +
                  end
         | 
| 185 | 
            +
                end
         | 
| 186 | 
            +
              end
         | 
| 187 | 
            +
             | 
| 188 | 
            +
              # Fix Apartment's lack of support for load_async
         | 
| 189 | 
            +
              ::ActiveRecord::FutureResult.prepend CorePatches::ActiveRecord::FutureResult
         | 
| 190 | 
            +
             | 
| 191 | 
            +
              # Hack ActiveRecord shards/connection_pooling to support our multi-DB approach
         | 
| 192 | 
            +
              ::ActiveRecord::Base.include CorePatches::ActiveRecord::Base
         | 
| 193 | 
            +
            end
         | 
| 194 | 
            +
             | 
| 8 195 | 
             
            Apartment.configure do |config|
         | 
| 9 196 | 
             
              config.excluded_models ||= []
         | 
| 10 197 | 
             
              config.excluded_models |= ['PandaPal::Organization', 'PandaPal::Session']
         | 
| 11 198 |  | 
| 12 | 
            -
              config. | 
| 13 | 
            -
             | 
| 14 | 
            -
               | 
| 199 | 
            +
              config.with_multi_server_setup = true unless Rails.env.test?
         | 
| 200 | 
            +
             | 
| 201 | 
            +
              config.tenant_names = lambda do
         | 
| 202 | 
            +
                if PandaPal::Organization < PandaPal::OrganizationConcerns::MultiDatabaseSharding
         | 
| 203 | 
            +
                  base_config = Apartment.connection_config
         | 
| 204 | 
            +
                  shard_configurations = Apartment.shard_configurations
         | 
| 205 | 
            +
             | 
| 206 | 
            +
                  PandaPal::Organization.all.to_a.each_with_object({}) do |org, hash|
         | 
| 207 | 
            +
                    shard = org.shard || "default"
         | 
| 208 | 
            +
                    hash[org.tenant_name] = shard == "default" ? base_config : shard_configurations[shard.downcase]
         | 
| 209 | 
            +
                  end
         | 
| 210 | 
            +
                else
         | 
| 211 | 
            +
                  PandaPal::Organization.pluck(:name)
         | 
| 212 | 
            +
                end
         | 
| 213 | 
            +
              end
         | 
| 15 214 | 
             
            end
         | 
| 16 215 |  | 
| 17 216 | 
             
            Rails.application.config.middleware.use Apartment::Elevators::Generic, lambda { |request|
         | 
    
        data/lib/panda_pal/engine.rb
    CHANGED
    
    
| @@ -0,0 +1,82 @@ | |
| 1 | 
            +
            module PandaPal::SpecHelper
         | 
| 2 | 
            +
              extend ActiveSupport::Concern
         | 
| 3 | 
            +
             | 
| 4 | 
            +
              class_methods do
         | 
| 5 | 
            +
                def with_multiple_shards(shards:, preclean_schemas: [], determine_shard: nil, transactional: true)
         | 
| 6 | 
            +
                  self.use_transactional_tests = transactional
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  original_settings = {}
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  before :all do
         | 
| 11 | 
            +
                    if defined?(DatabaseCleaner)
         | 
| 12 | 
            +
                      original_settings[:cleaner_strategy] = DatabaseCleaner.strategy
         | 
| 13 | 
            +
                      DatabaseCleaner.strategy = :truncation
         | 
| 14 | 
            +
                    end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                    Apartment.configure do |config|
         | 
| 17 | 
            +
                      original_settings[:with_multi_server_setup] = config.with_multi_server_setup
         | 
| 18 | 
            +
                      config.with_multi_server_setup = true
         | 
| 19 | 
            +
                    end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                    shards.each do |k, v|
         | 
| 22 | 
            +
                      Apartment.shard_configurations[k.to_s] = Apartment.connection_config.merge(database: v)
         | 
| 23 | 
            +
                    end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                    @existing_org_ids = PandaPal::Organization.pluck(:id)
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                    Apartment::Tenant.reload!
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                    clean_schemas(preclean_schemas)
         | 
| 30 | 
            +
                  end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                  after :all do
         | 
| 33 | 
            +
                    Apartment::Tenant.reset
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                    Apartment.configure do |config|
         | 
| 36 | 
            +
                      config.with_multi_server_setup = false
         | 
| 37 | 
            +
                    end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                    shards.each do |k, _|
         | 
| 40 | 
            +
                      Apartment.shard_configurations.delete(k.to_s)
         | 
| 41 | 
            +
                    end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                    PandaPal::Organization.where.not(id: @existing_org_ids).delete_all
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                    if defined?(DatabaseCleaner)
         | 
| 46 | 
            +
                      DatabaseCleaner.strategy = original_settings[:cleaner_strategy]
         | 
| 47 | 
            +
                    end
         | 
| 48 | 
            +
                  end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                  before :each do
         | 
| 51 | 
            +
                    clean_schemas(preclean_schemas) unless transactional
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                    @each_existing_org_ids = PandaPal::Organization.pluck(:id)
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                    if determine_shard
         | 
| 56 | 
            +
                      allow_any_instance_of(PandaPal::Organization).to receive(:tenant_name) do |org|
         | 
| 57 | 
            +
                        shard = determine_shard.call(org)
         | 
| 58 | 
            +
                        tenant = org.name
         | 
| 59 | 
            +
                        tenant = "#{shard}:#{tenant}" if shard.present?
         | 
| 60 | 
            +
                        tenant
         | 
| 61 | 
            +
                      end
         | 
| 62 | 
            +
                    end
         | 
| 63 | 
            +
                  end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                  after :each do
         | 
| 66 | 
            +
                    Apartment::Tenant.reset
         | 
| 67 | 
            +
                    PandaPal::Organization.where.not(id: @each_existing_org_ids).destroy_all
         | 
| 68 | 
            +
                  end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                  define_method :clean_schemas do |schemas|
         | 
| 71 | 
            +
                    PandaPal::Organization.where(name: schemas).delete_all
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                    schemas.each do |schema|
         | 
| 74 | 
            +
                      Apartment::Tenant.drop(schema) rescue nil
         | 
| 75 | 
            +
                      shards.each do |k, v|
         | 
| 76 | 
            +
                        Apartment::Tenant.drop("#{k}:#{schema}") rescue nil
         | 
| 77 | 
            +
                      end
         | 
| 78 | 
            +
                    end
         | 
| 79 | 
            +
                  end
         | 
| 80 | 
            +
                end
         | 
| 81 | 
            +
              end
         | 
| 82 | 
            +
            end
         | 
    
        data/lib/panda_pal/version.rb
    CHANGED
    
    
    
        data/panda_pal.gemspec
    CHANGED
    
    | @@ -21,7 +21,7 @@ Gem::Specification.new do |s| | |
| 21 21 | 
             
              s.test_files = Dir["spec/**/*"].reject{|f| f =~ /\/log\//}
         | 
| 22 22 |  | 
| 23 23 | 
             
              s.add_dependency "rails", ">= 4.2"
         | 
| 24 | 
            -
              s.add_dependency 'ros-apartment', '~>  | 
| 24 | 
            +
              s.add_dependency 'ros-apartment', '~> 3.0'
         | 
| 25 25 | 
             
              s.add_dependency 'ims-lti', '~> 1.2.4'
         | 
| 26 26 | 
             
              s.add_dependency 'browser', '2.5.0'
         | 
| 27 27 | 
             
              s.add_dependency 'attr_encrypted', '~> 4.0.0'
         | 
| @@ -30,11 +30,11 @@ Gem::Specification.new do |s| | |
| 30 30 | 
             
              s.add_dependency 'jwt'
         | 
| 31 31 | 
             
              s.add_dependency 'httparty'
         | 
| 32 32 |  | 
| 33 | 
            -
              s.add_development_dependency "rails", "~>  | 
| 33 | 
            +
              s.add_development_dependency "rails", "~> 7.0"
         | 
| 34 34 | 
             
              s.add_development_dependency 'pg'
         | 
| 35 35 | 
             
              s.add_development_dependency 'sidekiq', "< 7.0"
         | 
| 36 36 | 
             
              s.add_development_dependency 'sidekiq-scheduler'
         | 
| 37 | 
            -
              s.add_development_dependency 'ros-apartment', '~>  | 
| 37 | 
            +
              s.add_development_dependency 'ros-apartment', '~> 3.0'
         | 
| 38 38 | 
             
              s.add_development_dependency 'ros-apartment-sidekiq', '~> 1.2'
         | 
| 39 39 | 
             
              s.add_development_dependency 'rspec-rails'
         | 
| 40 40 | 
             
              s.add_development_dependency 'factory_girl_rails'
         | 
| @@ -0,0 +1,48 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'rails_helper'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            RSpec.describe PandaPal::Organization, type: :model do
         | 
| 6 | 
            +
              context "across multiple DBs" do
         | 
| 7 | 
            +
                with_multiple_shards(
         | 
| 8 | 
            +
                  shards: { alt: "panda_pal_test2" },
         | 
| 9 | 
            +
                  preclean_schemas: %w[on_primary on_alt],
         | 
| 10 | 
            +
                  transactional: false,
         | 
| 11 | 
            +
                  determine_shard: ->(org) { org.name.include?("alt") ? "alt" : nil }
         | 
| 12 | 
            +
                )
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                let!(:org1) { create(:panda_pal_organization, name: "on_primary") }
         | 
| 15 | 
            +
                let!(:org2) { create(:panda_pal_organization, name: "on_alt") }
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                it "successfully creates tenants on multiple shards" do
         | 
| 18 | 
            +
                  expect(1).to eql(1)
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                it "only creates schemas on the target shard" do
         | 
| 22 | 
            +
                  org1.switch_tenant do
         | 
| 23 | 
            +
                    schemas = PandaPal::ApiCall.connection.exec_query("SELECT schema_name FROM information_schema.schemata").pluck("schema_name")
         | 
| 24 | 
            +
                    expect(schemas).to include("on_primary")
         | 
| 25 | 
            +
                    expect(schemas).to_not include("on_alt")
         | 
| 26 | 
            +
                  end
         | 
| 27 | 
            +
                  org2.switch_tenant do
         | 
| 28 | 
            +
                    schemas = PandaPal::ApiCall.connection.exec_query("SELECT schema_name FROM information_schema.schemata").pluck("schema_name")
         | 
| 29 | 
            +
                    expect(schemas).to include("on_alt")
         | 
| 30 | 
            +
                    expect(schemas).to_not include("on_primary")
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                context "load_async" do
         | 
| 35 | 
            +
                  it "works across shards" do
         | 
| 36 | 
            +
                    qs = []
         | 
| 37 | 
            +
                    PandaPal::Organization.all.each do |org|
         | 
| 38 | 
            +
                      org.switch_tenant do
         | 
| 39 | 
            +
                        PandaPal::ApiCall.create!(logic: org.name)
         | 
| 40 | 
            +
                        qs << PandaPal::ApiCall.select("logic, pg_sleep(0.5)").load_async
         | 
| 41 | 
            +
                      end
         | 
| 42 | 
            +
                    end
         | 
| 43 | 
            +
                    calls = qs.map(&:to_a).flatten
         | 
| 44 | 
            +
                    expect(calls.pluck(:logic)).to match_array(["on_primary", "on_alt"])
         | 
| 45 | 
            +
                  end
         | 
| 46 | 
            +
                end
         | 
| 47 | 
            +
              end
         | 
| 48 | 
            +
            end
         | 
| @@ -14,8 +14,12 @@ development: | |
| 14 14 | 
             
              database: panda_pal_development
         | 
| 15 15 |  | 
| 16 16 | 
             
            test:
         | 
| 17 | 
            -
               | 
| 18 | 
            -
             | 
| 17 | 
            +
              test1:
         | 
| 18 | 
            +
                <<: *default
         | 
| 19 | 
            +
                database: panda_pal_test1
         | 
| 20 | 
            +
              test2:
         | 
| 21 | 
            +
                <<: *default
         | 
| 22 | 
            +
                database: panda_pal_test2
         | 
| 19 23 |  | 
| 20 24 | 
             
            production:
         | 
| 21 25 | 
             
              <<: *default
         | 
| @@ -37,6 +37,11 @@ Rails.application.configure do | |
| 37 37 | 
             
              # Print deprecation notices to the stderr.
         | 
| 38 38 | 
             
              config.active_support.deprecation = :stderr
         | 
| 39 39 |  | 
| 40 | 
            +
              config.log_level = :debug
         | 
| 41 | 
            +
              config.active_record.verbose_query_logs = true
         | 
| 42 | 
            +
              config.active_record.async_query_executor = :multi_thread_pool
         | 
| 43 | 
            +
              # config.active_record.global_executor_concurrency = 4
         | 
| 44 | 
            +
             | 
| 40 45 | 
             
              # Raises error for missing translations
         | 
| 41 46 | 
             
              # config.action_view.raise_on_missing_translations = true
         | 
| 42 47 | 
             
            end
         | 
| @@ -0,0 +1,18 @@ | |
| 1 | 
            +
            # This file is auto-generated from the current state of the database. Instead
         | 
| 2 | 
            +
            # of editing this file, please use the migrations feature of Active Record to
         | 
| 3 | 
            +
            # incrementally modify your database, and then regenerate this schema definition.
         | 
| 4 | 
            +
            #
         | 
| 5 | 
            +
            # Note that this schema.rb definition is the authoritative source for your
         | 
| 6 | 
            +
            # database schema. If you need to create the application database on another
         | 
| 7 | 
            +
            # system, you should be using db:schema:load, not running all the migrations
         | 
| 8 | 
            +
            # from scratch. The latter is a flawed and unsustainable approach (the more migrations
         | 
| 9 | 
            +
            # you'll amass, the slower it'll run and the greater likelihood for issues).
         | 
| 10 | 
            +
            #
         | 
| 11 | 
            +
            # It's strongly recommended that you check this file into your version control system.
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            ActiveRecord::Schema.define(version: 2022_07_21_095653) do
         | 
| 14 | 
            +
             | 
| 15 | 
            +
              # These are extensions that must be enabled in order to support this database
         | 
| 16 | 
            +
              enable_extension "plpgsql"
         | 
| 17 | 
            +
             | 
| 18 | 
            +
            end
         | 
| @@ -5,6 +5,10 @@ module PandaPal | |
| 5 5 | 
             
              RSpec.describe OrganizationConcerns::SettingsValidation, type: :model do
         | 
| 6 6 | 
             
                let!(:org) { create :panda_pal_organization }
         | 
| 7 7 |  | 
| 8 | 
            +
                after :all do
         | 
| 9 | 
            +
                  PandaPal.lti_options[:settings_structure] = nil
         | 
| 10 | 
            +
                end
         | 
| 11 | 
            +
             | 
| 8 12 | 
             
                def set_test_settings_structure
         | 
| 9 13 | 
             
                  PandaPal.lti_options = {
         | 
| 10 14 | 
             
                    title: 'Test App',
         | 
| 
            File without changes
         | 
    
        data/spec/rails_helper.rb
    CHANGED
    
    | @@ -1,6 +1,10 @@ | |
| 1 1 | 
             
            ActiveRecord::Migration.maintain_test_schema!
         | 
| 2 2 |  | 
| 3 | 
            +
            require "panda_pal/spec_helper"
         | 
| 4 | 
            +
             | 
| 3 5 | 
             
            RSpec.configure do |config|
         | 
| 6 | 
            +
              config.include PandaPal::SpecHelper
         | 
| 7 | 
            +
             | 
| 4 8 | 
             
              # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
         | 
| 5 9 | 
             
              config.fixture_path = "#{::Rails.root}/spec/fixtures"
         | 
| 6 10 |  | 
    
        data/spec/spec_helper.rb
    CHANGED
    
    
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: panda_pal
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 5. | 
| 4 | 
            +
              version: 5.12.0
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Instructure CustomDev
         | 
| 8 | 
            -
            autorequire: | 
| 8 | 
            +
            autorequire:
         | 
| 9 9 | 
             
            bindir: bin
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2024- | 
| 11 | 
            +
            date: 2024-11-26 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: rails
         | 
| @@ -30,14 +30,14 @@ dependencies: | |
| 30 30 | 
             
                requirements:
         | 
| 31 31 | 
             
                - - "~>"
         | 
| 32 32 | 
             
                  - !ruby/object:Gem::Version
         | 
| 33 | 
            -
                    version: ' | 
| 33 | 
            +
                    version: '3.0'
         | 
| 34 34 | 
             
              type: :runtime
         | 
| 35 35 | 
             
              prerelease: false
         | 
| 36 36 | 
             
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 37 37 | 
             
                requirements:
         | 
| 38 38 | 
             
                - - "~>"
         | 
| 39 39 | 
             
                  - !ruby/object:Gem::Version
         | 
| 40 | 
            -
                    version: ' | 
| 40 | 
            +
                    version: '3.0'
         | 
| 41 41 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 42 42 | 
             
              name: ims-lti
         | 
| 43 43 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| @@ -142,14 +142,14 @@ dependencies: | |
| 142 142 | 
             
                requirements:
         | 
| 143 143 | 
             
                - - "~>"
         | 
| 144 144 | 
             
                  - !ruby/object:Gem::Version
         | 
| 145 | 
            -
                    version: ' | 
| 145 | 
            +
                    version: '7.0'
         | 
| 146 146 | 
             
              type: :development
         | 
| 147 147 | 
             
              prerelease: false
         | 
| 148 148 | 
             
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 149 149 | 
             
                requirements:
         | 
| 150 150 | 
             
                - - "~>"
         | 
| 151 151 | 
             
                  - !ruby/object:Gem::Version
         | 
| 152 | 
            -
                    version: ' | 
| 152 | 
            +
                    version: '7.0'
         | 
| 153 153 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 154 154 | 
             
              name: pg
         | 
| 155 155 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| @@ -198,14 +198,14 @@ dependencies: | |
| 198 198 | 
             
                requirements:
         | 
| 199 199 | 
             
                - - "~>"
         | 
| 200 200 | 
             
                  - !ruby/object:Gem::Version
         | 
| 201 | 
            -
                    version: ' | 
| 201 | 
            +
                    version: '3.0'
         | 
| 202 202 | 
             
              type: :development
         | 
| 203 203 | 
             
              prerelease: false
         | 
| 204 204 | 
             
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 205 205 | 
             
                requirements:
         | 
| 206 206 | 
             
                - - "~>"
         | 
| 207 207 | 
             
                  - !ruby/object:Gem::Version
         | 
| 208 | 
            -
                    version: ' | 
| 208 | 
            +
                    version: '3.0'
         | 
| 209 209 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 210 210 | 
             
              name: ros-apartment-sidekiq
         | 
| 211 211 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| @@ -282,7 +282,7 @@ dependencies: | |
| 282 282 | 
             
                - - '='
         | 
| 283 283 | 
             
                  - !ruby/object:Gem::Version
         | 
| 284 284 | 
             
                    version: 2.7.1
         | 
| 285 | 
            -
            description: | 
| 285 | 
            +
            description:
         | 
| 286 286 | 
             
            email:
         | 
| 287 287 | 
             
            - pseng@instructure.com
         | 
| 288 288 | 
             
            executables: []
         | 
| @@ -311,9 +311,11 @@ files: | |
| 311 311 | 
             
            - app/lib/panda_pal/lti_jwt_validator.rb
         | 
| 312 312 | 
             
            - app/models/panda_pal/api_call.rb
         | 
| 313 313 | 
             
            - app/models/panda_pal/organization.rb
         | 
| 314 | 
            +
            - app/models/panda_pal/organization_concerns/multi_database_sharding.rb
         | 
| 314 315 | 
             
            - app/models/panda_pal/organization_concerns/organization_builder.rb
         | 
| 315 316 | 
             
            - app/models/panda_pal/organization_concerns/settings_validation.rb
         | 
| 316 317 | 
             
            - app/models/panda_pal/organization_concerns/task_scheduling.rb
         | 
| 318 | 
            +
            - app/models/panda_pal/organization_concerns/tenant_handling.rb
         | 
| 317 319 | 
             
            - app/models/panda_pal/panda_pal_record.rb
         | 
| 318 320 | 
             
            - app/models/panda_pal/platform.rb
         | 
| 319 321 | 
             
            - app/models/panda_pal/platform/canvas.rb
         | 
| @@ -343,21 +345,23 @@ files: | |
| 343 345 | 
             
            - lib/panda_pal/helpers/secure_headers.rb
         | 
| 344 346 | 
             
            - lib/panda_pal/helpers/session_replacement.rb
         | 
| 345 347 | 
             
            - lib/panda_pal/plugins.rb
         | 
| 348 | 
            +
            - lib/panda_pal/spec_helper.rb
         | 
| 346 349 | 
             
            - lib/panda_pal/version.rb
         | 
| 347 350 | 
             
            - lib/tasks/panda_pal_tasks.rake
         | 
| 348 351 | 
             
            - panda_pal.gemspec
         | 
| 349 352 | 
             
            - spec/controllers/panda_pal/api_call_controller_spec.rb
         | 
| 353 | 
            +
            - spec/core/apartment_multidb_spec.rb
         | 
| 350 354 | 
             
            - spec/dummy/README.rdoc
         | 
| 351 355 | 
             
            - spec/dummy/Rakefile
         | 
| 352 356 | 
             
            - spec/dummy/app/assets/javascripts/application.js
         | 
| 353 357 | 
             
            - spec/dummy/app/assets/stylesheets/application.css
         | 
| 358 | 
            +
            - spec/dummy/app/bin/bundle
         | 
| 359 | 
            +
            - spec/dummy/app/bin/rails
         | 
| 360 | 
            +
            - spec/dummy/app/bin/rake
         | 
| 361 | 
            +
            - spec/dummy/app/bin/setup
         | 
| 354 362 | 
             
            - spec/dummy/app/controllers/application_controller.rb
         | 
| 355 363 | 
             
            - spec/dummy/app/helpers/application_helper.rb
         | 
| 356 364 | 
             
            - spec/dummy/app/views/layouts/application.html.erb
         | 
| 357 | 
            -
            - spec/dummy/bin/bundle
         | 
| 358 | 
            -
            - spec/dummy/bin/rails
         | 
| 359 | 
            -
            - spec/dummy/bin/rake
         | 
| 360 | 
            -
            - spec/dummy/bin/setup
         | 
| 361 365 | 
             
            - spec/dummy/config.ru
         | 
| 362 366 | 
             
            - spec/dummy/config/application.rb
         | 
| 363 367 | 
             
            - spec/dummy/config/boot.rb
         | 
| @@ -377,6 +381,7 @@ files: | |
| 377 381 | 
             
            - spec/dummy/config/routes.rb
         | 
| 378 382 | 
             
            - spec/dummy/config/secrets.yml
         | 
| 379 383 | 
             
            - spec/dummy/db/schema.rb
         | 
| 384 | 
            +
            - spec/dummy/db/test2_schema.rb
         | 
| 380 385 | 
             
            - spec/dummy/public/404.html
         | 
| 381 386 | 
             
            - spec/dummy/public/422.html
         | 
| 382 387 | 
             
            - spec/dummy/public/500.html
         | 
| @@ -394,7 +399,7 @@ homepage: http://instructure.com | |
| 394 399 | 
             
            licenses:
         | 
| 395 400 | 
             
            - MIT
         | 
| 396 401 | 
             
            metadata: {}
         | 
| 397 | 
            -
            post_install_message: | 
| 402 | 
            +
            post_install_message:
         | 
| 398 403 | 
             
            rdoc_options: []
         | 
| 399 404 | 
             
            require_paths:
         | 
| 400 405 | 
             
            - lib
         | 
| @@ -409,52 +414,54 @@ required_rubygems_version: !ruby/object:Gem::Requirement | |
| 409 414 | 
             
                - !ruby/object:Gem::Version
         | 
| 410 415 | 
             
                  version: '0'
         | 
| 411 416 | 
             
            requirements: []
         | 
| 412 | 
            -
            rubygems_version: 3. | 
| 413 | 
            -
            signing_key: | 
| 417 | 
            +
            rubygems_version: 3.5.16
         | 
| 418 | 
            +
            signing_key:
         | 
| 414 419 | 
             
            specification_version: 4
         | 
| 415 420 | 
             
            summary: LTI mountable engine
         | 
| 416 421 | 
             
            test_files:
         | 
| 417 | 
            -
            - spec/ | 
| 418 | 
            -
            - spec/ | 
| 419 | 
            -
            - spec/dummy/ | 
| 422 | 
            +
            - spec/controllers/panda_pal/api_call_controller_spec.rb
         | 
| 423 | 
            +
            - spec/core/apartment_multidb_spec.rb
         | 
| 424 | 
            +
            - spec/dummy/README.rdoc
         | 
| 425 | 
            +
            - spec/dummy/Rakefile
         | 
| 420 426 | 
             
            - spec/dummy/app/assets/javascripts/application.js
         | 
| 421 427 | 
             
            - spec/dummy/app/assets/stylesheets/application.css
         | 
| 428 | 
            +
            - spec/dummy/app/bin/bundle
         | 
| 429 | 
            +
            - spec/dummy/app/bin/rails
         | 
| 430 | 
            +
            - spec/dummy/app/bin/rake
         | 
| 431 | 
            +
            - spec/dummy/app/bin/setup
         | 
| 432 | 
            +
            - spec/dummy/app/controllers/application_controller.rb
         | 
| 422 433 | 
             
            - spec/dummy/app/helpers/application_helper.rb
         | 
| 423 | 
            -
            - spec/dummy/ | 
| 424 | 
            -
            - spec/dummy/bin/setup
         | 
| 425 | 
            -
            - spec/dummy/bin/bundle
         | 
| 426 | 
            -
            - spec/dummy/bin/rails
         | 
| 427 | 
            -
            - spec/dummy/config/secrets.yml
         | 
| 428 | 
            -
            - spec/dummy/config/routes.rb
         | 
| 429 | 
            -
            - spec/dummy/config/locales/en.yml
         | 
| 430 | 
            -
            - spec/dummy/config/environments/production.rb
         | 
| 431 | 
            -
            - spec/dummy/config/environments/development.rb
         | 
| 432 | 
            -
            - spec/dummy/config/environments/test.rb
         | 
| 433 | 
            -
            - spec/dummy/config/environment.rb
         | 
| 434 | 
            +
            - spec/dummy/app/views/layouts/application.html.erb
         | 
| 434 435 | 
             
            - spec/dummy/config/application.rb
         | 
| 435 | 
            -
            - spec/dummy/config/database.yml
         | 
| 436 436 | 
             
            - spec/dummy/config/boot.rb
         | 
| 437 | 
            +
            - spec/dummy/config/database.yml
         | 
| 438 | 
            +
            - spec/dummy/config/environment.rb
         | 
| 439 | 
            +
            - spec/dummy/config/environments/development.rb
         | 
| 440 | 
            +
            - spec/dummy/config/environments/production.rb
         | 
| 441 | 
            +
            - spec/dummy/config/environments/test.rb
         | 
| 437 442 | 
             
            - spec/dummy/config/initializers/backtrace_silencers.rb
         | 
| 438 | 
            -
            - spec/dummy/config/initializers/ | 
| 443 | 
            +
            - spec/dummy/config/initializers/cookies_serializer.rb
         | 
| 439 444 | 
             
            - spec/dummy/config/initializers/filter_parameter_logging.rb
         | 
| 445 | 
            +
            - spec/dummy/config/initializers/inflections.rb
         | 
| 446 | 
            +
            - spec/dummy/config/initializers/mime_types.rb
         | 
| 440 447 | 
             
            - spec/dummy/config/initializers/session_store.rb
         | 
| 441 448 | 
             
            - spec/dummy/config/initializers/wrap_parameters.rb
         | 
| 442 | 
            -
            - spec/dummy/config/ | 
| 443 | 
            -
            - spec/dummy/config/ | 
| 449 | 
            +
            - spec/dummy/config/locales/en.yml
         | 
| 450 | 
            +
            - spec/dummy/config/routes.rb
         | 
| 451 | 
            +
            - spec/dummy/config/secrets.yml
         | 
| 444 452 | 
             
            - spec/dummy/config.ru
         | 
| 445 | 
            -
            - spec/dummy/ | 
| 446 | 
            -
            - spec/dummy/ | 
| 453 | 
            +
            - spec/dummy/db/schema.rb
         | 
| 454 | 
            +
            - spec/dummy/db/test2_schema.rb
         | 
| 455 | 
            +
            - spec/dummy/public/404.html
         | 
| 447 456 | 
             
            - spec/dummy/public/422.html
         | 
| 448 457 | 
             
            - spec/dummy/public/500.html
         | 
| 449 | 
            -
            - spec/dummy/public/ | 
| 450 | 
            -
            - spec/ | 
| 451 | 
            -
            - spec/ | 
| 458 | 
            +
            - spec/dummy/public/favicon.ico
         | 
| 459 | 
            +
            - spec/factories/panda_pal_organizations.rb
         | 
| 460 | 
            +
            - spec/factories/panda_pal_sessions.rb
         | 
| 461 | 
            +
            - spec/models/panda_pal/api_call_spec.rb
         | 
| 452 462 | 
             
            - spec/models/panda_pal/organization/settings_validation_spec.rb
         | 
| 453 463 | 
             
            - spec/models/panda_pal/organization/task_scheduling_spec.rb
         | 
| 454 | 
            -
            - spec/models/panda_pal/session_spec.rb
         | 
| 455 | 
            -
            - spec/models/panda_pal/api_call_spec.rb
         | 
| 456 464 | 
             
            - spec/models/panda_pal/organization_spec.rb
         | 
| 457 | 
            -
            - spec/ | 
| 458 | 
            -
            - spec/factories/panda_pal_organizations.rb
         | 
| 459 | 
            -
            - spec/controllers/panda_pal/api_call_controller_spec.rb
         | 
| 465 | 
            +
            - spec/models/panda_pal/session_spec.rb
         | 
| 460 466 | 
             
            - spec/rails_helper.rb
         | 
| 467 | 
            +
            - spec/spec_helper.rb
         | 
| 
            File without changes
         | 
| 
            File without changes
         | 
| 
            File without changes
         | 
| 
            File without changes
         |