Skip to content

Commit

Permalink
Merge pull request #18102 from lpichler/dynamic_product_features
Browse files Browse the repository at this point in the history
Dynamic product features according to tenants
  • Loading branch information
gtanzillo authored Oct 30, 2018
2 parents e513775 + cd56be0 commit e391dec
Show file tree
Hide file tree
Showing 8 changed files with 287 additions and 15 deletions.
52 changes: 48 additions & 4 deletions app/models/miq_product_feature.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class MiqProductFeature < ApplicationRecord
has_and_belongs_to_many :miq_user_roles, :join_table => :miq_roles_features
has_many :miq_product_features_shares
has_many :shares, :through => :miq_product_features_shares
belongs_to :tenant

virtual_delegate :identifier, :to => :parent, :prefix => true, :allow_nil => true

Expand All @@ -22,13 +23,37 @@ class MiqProductFeature < ApplicationRecord
:description,
:feature_type,
:hidden,
:protected
:protected,
:tenant_id
]

FEATURE_TYPE_ORDER = ["view", "control", "admin", "node"]
FEATURE_TYPE_ORDER = %w(view control admin node).freeze
REQUIRED_ATTRIBUTES = [:identifier].freeze
OPTIONAL_ATTRIBUTES = [:name, :feature_type, :description, :children, :hidden, :protected].freeze
OPTIONAL_ATTRIBUTES = %i(name feature_type description children hidden protected).freeze
ALLOWED_ATTRIBUTES = (REQUIRED_ATTRIBUTES + OPTIONAL_ATTRIBUTES).freeze
TENANT_FEATURE_ROOT_IDENTIFIERS = %w(dialog_new_editor dialog_edit_editor dialog_copy_editor dialog_delete).freeze

def name
value = self[:name]
self[:tenant_id] ? "#{value} (#{tenant.name})" : value
end

def description
value = self[:description]
self[:tenant_id] ? "#{value} for tenant #{tenant.name}" : value
end

def self.tenant_identifier(identifier, tenant_id)
"#{identifier}_tenant_#{tenant_id}"
end

def self.root_tenant_identifier?(identifier)
TENANT_FEATURE_ROOT_IDENTIFIERS.include?(identifier)
end

def self.current_tenant_identifier(identifier)
identifier && feature_details(identifier) && root_tenant_identifier?(identifier) ? tenant_identifier(identifier, User.current_tenant.id) : identifier
end

def self.feature_yaml(path = FIXTURE_PATH)
"#{path}.yml".freeze
Expand Down Expand Up @@ -74,6 +99,12 @@ def self.feature_exists?(ident)
features.key?(ident)
end

def self.invalidate_caches
@feature_cache = nil
@obj_cache = nil
@detail = nil
end

def self.features
@feature_cache ||= begin
# create hash with parent identifier and details
Expand Down Expand Up @@ -106,6 +137,18 @@ def self.seed
seed_features
end

def self.with_tenant_feature_root_features
where(:identifier => TENANT_FEATURE_ROOT_IDENTIFIERS)
end

def self.seed_tenant_miq_product_features
result = with_tenant_feature_root_features.map.each do |tenant_miq_product_feature|
Tenant.all.map { |tenant| tenant.build_miq_product_feature(tenant_miq_product_feature) }
end.flatten

MiqProductFeature.create(result).map(&:identifier)
end

def self.seed_features(path = FIXTURE_PATH)
fixture_yaml = feature_yaml(path)

Expand All @@ -117,7 +160,8 @@ def self.seed_features(path = FIXTURE_PATH)
seed_from_hash(YAML.load_file(fixture), seen, root_feature)
end

deletes = where.not(:identifier => seen.values.flatten).destroy_all
tenant_identifiers = seed_tenant_miq_product_features
deletes = where.not(:identifier => seen.values.flatten + tenant_identifiers).destroy_all
_log.info("Deleting product features: #{deletes.collect(&:identifier).inspect}") unless deletes.empty?
seen
end
Expand Down
27 changes: 26 additions & 1 deletion app/models/tenant.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class Tenant < ApplicationRecord
has_many :services, :dependent => :destroy
has_many :shares
has_many :authentications, :dependent => :nullify
has_many :miq_product_features, :dependent => :destroy

belongs_to :default_miq_group, :class_name => "MiqGroup", :dependent => :destroy
belongs_to :source, :polymorphic => true
Expand All @@ -57,7 +58,7 @@ class Tenant < ApplicationRecord
virtual_column :display_type, :type => :string

before_save :nil_blanks
after_create :create_tenant_group
after_create :create_tenant_group, :create_miq_product_features_for_tenant_nodes
before_destroy :ensure_can_be_destroyed

def self.scope_by_tenant?
Expand Down Expand Up @@ -290,6 +291,30 @@ def allowed?
Rbac::Filterer.filtered_object(self).present?
end

def create_miq_product_features_for_tenant_nodes
result = MiqProductFeature.with_tenant_feature_root_features.map.each do |tenant_miq_product_feature|
build_miq_product_feature(tenant_miq_product_feature)
end.flatten

result = MiqProductFeature.create(result).map(&:identifier)

MiqProductFeature.invalidate_caches
result
end

def build_miq_product_feature(miq_product_feature)
attrs = {
:name => miq_product_feature.name, :description => miq_product_feature.description,
:feature_type => 'admin',
:hidden => false,
:identifier => MiqProductFeature.tenant_identifier(miq_product_feature.identifier, id),
:tenant_id => id,
:parent_id => miq_product_feature.id
}

attrs
end

private

# when a root tenant has an attribute with a nil value,
Expand Down
12 changes: 6 additions & 6 deletions db/fixtures/miq_product_features.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2440,34 +2440,34 @@
- :name: Dialogs
:description: Dialogs Accordion
:feature_type: node
:hidden: true
:hidden: false
:identifier: dialog_accord
:children:
- :name: Modify
:description: Modify Dialogs
:feature_type: admin
:identifier: dialog_admin
:hidden: true
:hidden: false
:children:
- :name: Add
:description: Add Dialog in the Dialog Editor
:feature_type: admin
:hidden: true
:hidden: false
:identifier: dialog_new_editor
- :name: Edit
:description: Edit Dialog in the Dialog Editor
:feature_type: admin
:hidden: true
:hidden: false
:identifier: dialog_edit_editor
- :name: Copy
:description: Copy Dialog
:feature_type: admin
:hidden: true
:hidden: false
:identifier: dialog_copy_editor
- :name: Delete
:description: Delete Dialog
:feature_type: admin
:hidden: true
:hidden: false
:identifier: dialog_delete
- :name: Provisioning Dialogs
:description: Provisioning Dialogs Accordion
Expand Down
2 changes: 2 additions & 0 deletions lib/rbac/authorizer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ def role_allows?(options = {})

any = options[:any]

identifier = MiqProductFeature.current_tenant_identifier(identifier)

auth = if any.present?
user_role_allows_any?(user, :identifiers => (identifiers || [identifier]))
else
Expand Down
146 changes: 142 additions & 4 deletions spec/models/miq_product_feature_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@
require 'pathname'

describe MiqProductFeature do
let(:miq_product_feature_class) do
Class.new(described_class) do
def self.with_parent_tenant_nodes
includes(:parent).where(:parents_miq_product_features => {:identifier => self::TENANT_FEATURE_ROOT_IDENTIFIERS})
end

def self.tenant_features_in_hash
with_parent_tenant_nodes.map { |x| x.slice(:name, :description, :identifier, :tenant_id) }
end
end
end

# - container_dashboard
# - miq_report_widget_editor
# - miq_report_widget_admin
Expand Down Expand Up @@ -55,7 +67,7 @@ def traverse_product_features(product_feature, &block)
:children => [
{
:feature_type => "node",
:identifier => "one",
:identifier => "dialog_new_editor",
:name => "One",
:children => []
}
Expand All @@ -77,7 +89,7 @@ def traverse_product_features(product_feature, &block)

it "existing records" do
deleted = FactoryGirl.create(:miq_product_feature, :identifier => "xxx")
changed = FactoryGirl.create(:miq_product_feature, :identifier => "one", :name => "XXX")
changed = FactoryGirl.create(:miq_product_feature, :identifier => "dialog_new_editor", :name => "XXX")
unchanged = FactoryGirl.create(:miq_product_feature_everything)
unchanged_orig_updated_at = unchanged.updated_at

Expand All @@ -90,7 +102,7 @@ def traverse_product_features(product_feature, &block)
it "additional yaml feature" do
additional = {
:feature_type => "node",
:identifier => "two",
:identifier => "dialog_edit_editor",
:children => []
}

Expand All @@ -101,11 +113,137 @@ def traverse_product_features(product_feature, &block)

status_seed = MiqProductFeature.seed_features(feature_path)
expect(MiqProductFeature.count).to eq(3)
expect(status_seed[:created]).to match_array %w(everything one two)
expect(status_seed[:created]).to match_array %w(everything dialog_new_editor dialog_edit_editor)

additional_file.unlink
Dir.rmdir(feature_path)
end

context 'dynamic product features' do
context 'add new' do
let(:base) do
{
:feature_type => "node",
:identifier => "everything",
:children => [
{
:feature_type => "node",
:identifier => "one",
:name => "One",
:children => [
{
:feature_type => "admin",
:identifier => "dialog_copy_editor",
:name => "Edit",
:description => "XXX"
}
]
}
]
}
end

let(:root_tenant) do
Tenant.seed
Tenant.default_tenant
end

let!(:tenant) { FactoryGirl.create(:tenant, :parent => root_tenant) }

before do
MiqProductFeature.seed_features(feature_path)
end

it "creates tenant features" do
features = miq_product_feature_class.tenant_features_in_hash
expect(features).to match_array([{ "name" => "Edit (#{root_tenant.name})", "description" => "XXX for tenant #{root_tenant.name}",
"identifier" => "dialog_copy_editor_tenant_#{root_tenant.id}", "tenant_id" => root_tenant.id},
{"name" => "Edit (#{tenant.name})", "description" => "XXX for tenant #{tenant.name}",
"identifier" => "dialog_copy_editor_tenant_#{tenant.id}", "tenant_id" => tenant.id}])

expect(MiqProductFeature.where(:identifier => "dialog_copy_editor", :name => "Edit").count).to eq(1)
end

context "add tenant node product features" do
let(:base) do
{
:feature_type => "node",
:identifier => "everything",
:children => [
{
:feature_type => "node",
:identifier => "one",
:name => "One",
:children => [
{
:feature_type => "admin",
:identifier => "dialog_copy_editor",
:name => "Edit",
:description => "XXX"
}
]
},
{
:feature_type => "admin",
:identifier => "dialog_delete",
:name => "Add",
:description => "YYY"
}
]
}
end

it "add new tenant feature" do
features = miq_product_feature_class.tenant_features_in_hash
expect(features).to match_array([{ "name" => "Edit (#{root_tenant.name})", "description" => "XXX for tenant #{root_tenant.name}",
"identifier" => "dialog_copy_editor_tenant_#{root_tenant.id}", "tenant_id" => root_tenant.id},
{"name" => "Edit (#{tenant.name})", "description" => "XXX for tenant #{tenant.name}",
"identifier" => "dialog_copy_editor_tenant_#{tenant.id}", "tenant_id" => tenant.id},
{"name" => "Add (#{root_tenant.name})", "description" => "YYY for tenant #{root_tenant.name}",
"identifier" => "dialog_delete_tenant_#{root_tenant.id}", "tenant_id" => root_tenant.id},
{"name" => "Add (#{tenant.name})", "description" => "YYY for tenant #{tenant.name}",
"identifier" => "dialog_delete_tenant_#{tenant.id}", "tenant_id" => tenant.id}])

expect(MiqProductFeature.where(:identifier => "dialog_delete", :name => "Add").count).to eq(1)
end

context "remove added tenant feaure" do
let(:base) do
{
:feature_type => "node",
:identifier => "everything",
:children => [
{
:feature_type => "node",
:identifier => "one",
:name => "One",
:children => [
{
:feature_type => "admin",
:identifier => "dialog_copy_editor",
:name => "Edit",
:description => "XXX"
}
]
}
]
}
end

it "removes tenant features" do
features = miq_product_feature_class.tenant_features_in_hash

expect(features).to match_array([{ "name" => "Edit (#{root_tenant.name})", "description" => "XXX for tenant #{root_tenant.name}",
"identifier" => "dialog_copy_editor_tenant_#{root_tenant.id}", "tenant_id" => root_tenant.id},
{"name" => "Edit (#{tenant.name})", "description" => "XXX for tenant #{tenant.name}",
"identifier" => "dialog_copy_editor_tenant_#{tenant.id}", "tenant_id" => tenant.id}])

expect(MiqProductFeature.where(:identifier => "dialog_copy_editor", :name => "Edit").count).to eq(1)
end
end
end
end
end
end

describe '#feature_children' do
Expand Down
Loading

0 comments on commit e391dec

Please sign in to comment.