mirror of
https://github.com/lingble/chatwoot.git
synced 2025-10-30 18:47:51 +00:00
* Renamed concern from Feature to Featurable * Feature: Installation config (#839) * Added new model installtion config with corresponding migrations and specs * Created an installation config yml (key value store model) * Created a config loader module to load the installaltion configs * Added this to the config loader seeder * Changed the account before create hook for default feature enabling to use the feature values from installtion config * Renamed the feature concern to Featurable to follow the naming pattern for concerns * Added comments and specs for modules and places that deemed necessary * Refactored config loader to reduce cognitive complexity (#839)
This commit is contained in:
@@ -94,6 +94,8 @@ Rails/UniqueValidationWithoutIndex:
|
|||||||
Exclude:
|
Exclude:
|
||||||
- 'app/models/channel/twitter_profile.rb'
|
- 'app/models/channel/twitter_profile.rb'
|
||||||
- 'app/models/webhook.rb'
|
- 'app/models/webhook.rb'
|
||||||
|
RSpec/NamedSubject:
|
||||||
|
Enabled: false
|
||||||
AllCops:
|
AllCops:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'bin/**/*'
|
- 'bin/**/*'
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class Account < ApplicationRecord
|
|||||||
|
|
||||||
include Events::Types
|
include Events::Types
|
||||||
include Reportable
|
include Reportable
|
||||||
include Features
|
include Featurable
|
||||||
|
|
||||||
DEFAULT_QUERY_SETTING = {
|
DEFAULT_QUERY_SETTING = {
|
||||||
flag_query_mode: :bit_operator
|
flag_query_mode: :bit_operator
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
module Features
|
module Featurable
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
QUERY_MODE = {
|
QUERY_MODE = {
|
||||||
@@ -51,7 +51,10 @@ module Features
|
|||||||
private
|
private
|
||||||
|
|
||||||
def enable_default_features
|
def enable_default_features
|
||||||
features_to_enabled = FEATURE_LIST.select { |f| f['enabled'] }.map { |f| f['name'] }
|
config = InstallationConfig.find_by(name: 'ACCOUNT_LEVEL_FEATURE_DEFAULTS')
|
||||||
|
return true if config.blank?
|
||||||
|
|
||||||
|
features_to_enabled = config.value.select { |f| f[:enabled] }.map { |f| f[:name] }
|
||||||
enable_features(features_to_enabled)
|
enable_features(features_to_enabled)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
31
app/models/installation_config.rb
Normal file
31
app/models/installation_config.rb
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: installation_configs
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# name :string not null
|
||||||
|
# serialized_value :jsonb not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_installation_configs_on_name_and_created_at (name,created_at) UNIQUE
|
||||||
|
#
|
||||||
|
class InstallationConfig < ApplicationRecord
|
||||||
|
serialize :serialized_value, HashWithIndifferentAccess
|
||||||
|
|
||||||
|
validates :name, presence: true
|
||||||
|
|
||||||
|
default_scope { order(created_at: :desc) }
|
||||||
|
|
||||||
|
def value
|
||||||
|
serialized_value[:value]
|
||||||
|
end
|
||||||
|
|
||||||
|
def value=(value_to_assigned)
|
||||||
|
self.serialized_value = {
|
||||||
|
value: value_to_assigned
|
||||||
|
}.with_indifferent_access
|
||||||
|
end
|
||||||
|
end
|
||||||
2
config/installation_config.yml
Normal file
2
config/installation_config.yml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
- name: SHOW_WIDGET_HEADER
|
||||||
|
value: true
|
||||||
13
db/migrate/20200510112339_create_installation_config.rb
Normal file
13
db/migrate/20200510112339_create_installation_config.rb
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
class CreateInstallationConfig < ActiveRecord::Migration[6.0]
|
||||||
|
def change
|
||||||
|
create_table :installation_configs do |t|
|
||||||
|
t.string :name, null: false
|
||||||
|
t.jsonb :serialized_value, null: false, default: '{}'
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :installation_configs, [:name, :created_at], unique: true
|
||||||
|
|
||||||
|
ConfigLoader.new.process
|
||||||
|
end
|
||||||
|
end
|
||||||
24
db/schema.rb
24
db/schema.rb
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 2020_05_09_044639) do
|
ActiveRecord::Schema.define(version: 2020_05_10_112339) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "pgcrypto"
|
enable_extension "pgcrypto"
|
||||||
@@ -246,6 +246,14 @@ ActiveRecord::Schema.define(version: 2020_05_09_044639) do
|
|||||||
t.index ["account_id"], name: "index_inboxes_on_account_id"
|
t.index ["account_id"], name: "index_inboxes_on_account_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "installation_configs", force: :cascade do |t|
|
||||||
|
t.string "name", null: false
|
||||||
|
t.jsonb "serialized_value", default: "{}", null: false
|
||||||
|
t.datetime "created_at", precision: 6, null: false
|
||||||
|
t.datetime "updated_at", precision: 6, null: false
|
||||||
|
t.index ["name", "created_at"], name: "index_installation_configs_on_name_and_created_at", unique: true
|
||||||
|
end
|
||||||
|
|
||||||
create_table "messages", id: :serial, force: :cascade do |t|
|
create_table "messages", id: :serial, force: :cascade do |t|
|
||||||
t.text "content"
|
t.text "content"
|
||||||
t.integer "account_id", null: false
|
t.integer "account_id", null: false
|
||||||
@@ -319,20 +327,6 @@ ActiveRecord::Schema.define(version: 2020_05_09_044639) do
|
|||||||
t.boolean "payment_source_added", default: false
|
t.boolean "payment_source_added", default: false
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "super_admins", force: :cascade do |t|
|
|
||||||
t.string "email", default: "", null: false
|
|
||||||
t.string "encrypted_password", default: "", null: false
|
|
||||||
t.datetime "remember_created_at"
|
|
||||||
t.integer "sign_in_count", default: 0, null: false
|
|
||||||
t.datetime "current_sign_in_at"
|
|
||||||
t.datetime "last_sign_in_at"
|
|
||||||
t.inet "current_sign_in_ip"
|
|
||||||
t.inet "last_sign_in_ip"
|
|
||||||
t.datetime "created_at", precision: 6, null: false
|
|
||||||
t.datetime "updated_at", precision: 6, null: false
|
|
||||||
t.index ["email"], name: "index_super_admins_on_email", unique: true
|
|
||||||
end
|
|
||||||
|
|
||||||
create_table "taggings", id: :serial, force: :cascade do |t|
|
create_table "taggings", id: :serial, force: :cascade do |t|
|
||||||
t.integer "tag_id"
|
t.integer "tag_id"
|
||||||
t.string "taggable_type"
|
t.string "taggable_type"
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
# loading installation configs
|
||||||
|
ConfigLoader.new.process
|
||||||
|
|
||||||
account = Account.create!(
|
account = Account.create!(
|
||||||
name: 'Acme Inc',
|
name: 'Acme Inc',
|
||||||
domain: 'support.chatwoot.com',
|
domain: 'support.chatwoot.com',
|
||||||
|
|||||||
84
lib/config_loader.rb
Normal file
84
lib/config_loader.rb
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
class ConfigLoader
|
||||||
|
DEFAULT_OPTIONS = {
|
||||||
|
config_path: nil,
|
||||||
|
reconcile_only_new: true
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
def process(options = {})
|
||||||
|
options = DEFAULT_OPTIONS.merge(options)
|
||||||
|
# function of the "reconcile_only_new" flag
|
||||||
|
# if true,
|
||||||
|
# it leaves the existing config and feature flags as it is and
|
||||||
|
# creates the missing configs and feature flags with their default values
|
||||||
|
# if false,
|
||||||
|
# then it overwrites existing config and feature flags with default values
|
||||||
|
# also creates the missing configs and feature flags with their default values
|
||||||
|
@reconcile_only_new = options[:reconcile_only_new]
|
||||||
|
|
||||||
|
# setting the config path
|
||||||
|
@config_path = options[:config_path].presence
|
||||||
|
@config_path ||= Rails.root.join('config')
|
||||||
|
|
||||||
|
# general installation configs
|
||||||
|
reconcile_general_config
|
||||||
|
|
||||||
|
# default account based feature configs
|
||||||
|
reconcile_feature_config
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def general_configs
|
||||||
|
@general_configs ||= YAML.safe_load(File.read("#{@config_path}/installation_config.yml")).freeze
|
||||||
|
end
|
||||||
|
|
||||||
|
def account_features
|
||||||
|
@account_features ||= YAML.safe_load(File.read("#{@config_path}/features.yml")).freeze
|
||||||
|
end
|
||||||
|
|
||||||
|
def reconcile_general_config
|
||||||
|
general_configs.each do |config|
|
||||||
|
new_config = config.with_indifferent_access
|
||||||
|
existing_config = InstallationConfig.find_by(name: new_config[:name])
|
||||||
|
save_general_config(existing_config, new_config)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def save_general_config(existing_config, new_config)
|
||||||
|
if existing_config
|
||||||
|
# save config only if reconcile flag is false and existing configs value does not match default value
|
||||||
|
save_as_new_config(new_config) if !@reconcile_only_new && existing_config.value != new_config[:value]
|
||||||
|
else
|
||||||
|
save_as_new_config(new_config)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def save_as_new_config(new_config)
|
||||||
|
config = InstallationConfig.new(name: new_config[:name])
|
||||||
|
config.value = new_config[:value]
|
||||||
|
config.save
|
||||||
|
end
|
||||||
|
|
||||||
|
def reconcile_feature_config
|
||||||
|
config = InstallationConfig.find_by(name: 'ACCOUNT_LEVEL_FEATURE_DEFAULTS')
|
||||||
|
|
||||||
|
if config
|
||||||
|
return false if config.value.to_s == account_features.to_s
|
||||||
|
|
||||||
|
compare_and_save(config)
|
||||||
|
else
|
||||||
|
save_as_new_config({ name: 'ACCOUNT_LEVEL_FEATURE_DEFAULTS', value: account_features })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def compare_and_save_feature(config)
|
||||||
|
features = if @reconcile_only_new
|
||||||
|
# leave the existing feature flag values as it is and add new feature flags with default values
|
||||||
|
(account_features + config.value).uniq { |h| h['name'] }
|
||||||
|
else
|
||||||
|
# update the existing feature flag values with default values and add new feature flags with default values
|
||||||
|
(config.value + account_features).uniq { |h| h['name'] }
|
||||||
|
end
|
||||||
|
save_as_new_config({ name: 'ACCOUNT_LEVEL_FEATURE_DEFAULTS', value: features })
|
||||||
|
end
|
||||||
|
end
|
||||||
46
spec/lib/config_loader_spec.rb
Normal file
46
spec/lib/config_loader_spec.rb
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe ConfigLoader do
|
||||||
|
subject(:trigger) { described_class.new.process }
|
||||||
|
|
||||||
|
describe 'execute' do
|
||||||
|
context 'when called with default options' do
|
||||||
|
it 'creates installation configs' do
|
||||||
|
expect(InstallationConfig.count).to eq(0)
|
||||||
|
subject
|
||||||
|
expect(InstallationConfig.count).to be > 0
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates account level feature defaults as entry on config table' do
|
||||||
|
subject
|
||||||
|
expect(InstallationConfig.find_by(name: 'ACCOUNT_LEVEL_FEATURE_DEFAULTS')).to be_truthy
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with reconcile_only_new option' do
|
||||||
|
let(:class_instance) { described_class.new }
|
||||||
|
let(:config) { { name: 'WHO', value: 'corona' } }
|
||||||
|
let(:updated_config) { { name: 'WHO', value: 'covid 19' } }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(described_class).to receive(:new).and_return(class_instance)
|
||||||
|
allow(class_instance).to receive(:general_configs).and_return([config])
|
||||||
|
described_class.new.process
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'being true it should not update existing config value' do
|
||||||
|
expect(InstallationConfig.find_by(name: 'WHO').value).to eq('corona')
|
||||||
|
allow(class_instance).to receive(:general_configs).and_return([updated_config])
|
||||||
|
described_class.new.process({ reconcile_only_new: true })
|
||||||
|
expect(InstallationConfig.find_by(name: 'WHO').value).to eq('corona')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates the existing config value with new default value' do
|
||||||
|
expect(InstallationConfig.find_by(name: 'WHO').value).to eq('corona')
|
||||||
|
allow(class_instance).to receive(:general_configs).and_return([updated_config])
|
||||||
|
described_class.new.process({ reconcile_only_new: false })
|
||||||
|
expect(InstallationConfig.find_by(name: 'WHO').value).to eq('covid 19')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
7
spec/models/installation_config_spec.rb
Normal file
7
spec/models/installation_config_spec.rb
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe InstallationConfig do
|
||||||
|
it { is_expected.to validate_presence_of(:name) }
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user