Skip to content

Commit

Permalink
Support tenants for taggings (#1000)
Browse files Browse the repository at this point in the history
* support tenants for taggings

* Make tenant fully optional

* Move section of README to the correct place

Co-authored-by: Heng <heng@reamaze.com>
  • Loading branch information
lunaru and Heng committed May 19, 2021
1 parent 98a0822 commit 7e696e3
Show file tree
Hide file tree
Showing 11 changed files with 113 additions and 4 deletions.
23 changes: 23 additions & 0 deletions README.md
Expand Up @@ -80,6 +80,8 @@ Review the generated migrations then migrate :
rake db:migrate
```

If you do not wish or need to support multi-tenancy, the migration for `add_tenant_to_taggings` is optional and can be discarded safely.

#### For MySql users
You can circumvent at any time the problem of special characters [issue 623](https://github.com/mbleigh/acts-as-taggable-on/issues/623) by setting in an initializer file:

Expand Down Expand Up @@ -390,6 +392,27 @@ def remove_owned_tag
end
```

### Tag Tenancy

Tags support multi-tenancy. This is useful for applications where a Tag belongs to a scoped set of models:

```ruby
class Account < ActiveRecord::Base
has_many :photos
end

class User < ActiveRecord::Base
belongs_to :account
acts_as_taggable_on :tags
acts_as_taggable_tenant :account_id
end

@user1.tag_list = ["foo", "bar"] # these taggings will automatically have the tenant saved
@user2.tag_list = ["bar", "baz"]

ActsAsTaggableOn::Tag.for_tenant(@user1.account.id) # returns Tag models for "foo" and "bar", but not "baz"
```

### Dirty objects

```ruby
Expand Down
16 changes: 16 additions & 0 deletions db/migrate/7_add_tenant_to_taggings.rb
@@ -0,0 +1,16 @@
if ActiveRecord.gem_version >= Gem::Version.new('5.0')
class AddTenantToTaggings < ActiveRecord::Migration[4.2]; end
else
class AddTenantToTaggings < ActiveRecord::Migration; end
end
AddTenantToTaggings.class_eval do
def self.up
add_column :taggings, :tenant, :string, limit: 128
add_index :taggings, :tenant unless index_exists? :taggings, :tenant
end

def self.down
remove_column :taggings, :tenant
remove_index :taggings, :tenant
end
end
6 changes: 6 additions & 0 deletions lib/acts_as_taggable_on/tag.rb
Expand Up @@ -55,6 +55,12 @@ def self.for_context(context)
select("DISTINCT #{ActsAsTaggableOn.tags_table}.*")
end

def self.for_tenant(tenant)
joins(:taggings).
where("#{ActsAsTaggableOn.taggings_table}.tenant = ?", tenant.to_s).
select("DISTINCT #{ActsAsTaggableOn.tags_table}.*")
end

### CLASS METHODS:

def self.find_or_create_with_like_by_name(name)
Expand Down
18 changes: 18 additions & 0 deletions lib/acts_as_taggable_on/taggable.rb
Expand Up @@ -54,6 +54,23 @@ def acts_as_ordered_taggable_on(*tag_types)
taggable_on(true, tag_types)
end

def acts_as_taggable_tenant(tenant)
if taggable?
self.tenant_column = tenant
else
class_attribute :tenant_column
self.tenant_column = tenant
end

# each of these add context-specific methods and must be
# called on each call of taggable_on
include Core
include Collection
include Cache
include Ownership
include Related
end

private

# Make a model taggable on specified contexts
Expand All @@ -78,6 +95,7 @@ def taggable_on(preserve_tag_order, *tag_types)
self.tag_types = tag_types
class_attribute :preserve_tag_order
self.preserve_tag_order = preserve_tag_order
class_attribute :tenant_column

class_eval do
has_many :taggings, as: :taggable, dependent: :destroy, class_name: '::ActsAsTaggableOn::Tagging'
Expand Down
12 changes: 11 additions & 1 deletion lib/acts_as_taggable_on/taggable/core.rb
Expand Up @@ -214,6 +214,12 @@ def tagging_contexts
self.class.tag_types.map(&:to_s) + custom_contexts
end

def tenant
if self.class.tenant_column
read_attribute(self.class.tenant_column)
end
end

def reload(*args)
self.class.tag_types.each do |context|
instance_variable_set("@#{context.to_s.singularize}_list", nil)
Expand Down Expand Up @@ -272,7 +278,11 @@ def save_tags

# Create new taggings:
new_tags.each do |tag|
taggings.create!(tag_id: tag.id, context: context.to_s, taggable: self)
if tenant
taggings.create!(tag_id: tag.id, context: context.to_s, taggable: self, tenant: tenant)
else
taggings.create!(tag_id: tag.id, context: context.to_s, taggable: self)
end
end
end

Expand Down
2 changes: 2 additions & 0 deletions lib/acts_as_taggable_on/tagging.rb
Expand Up @@ -14,6 +14,8 @@ class Tagging < ::ActiveRecord::Base #:nodoc:
scope :by_contexts, ->(contexts) { where(context: (contexts || DEFAULT_CONTEXT)) }
scope :by_context, ->(context = DEFAULT_CONTEXT) { by_contexts(context.to_s) }

scope :by_tenant, ->(tenant) { where(tenant: tenant) }

validates_presence_of :context
validates_presence_of :tag_id

Expand Down
17 changes: 16 additions & 1 deletion spec/acts_as_taggable_on/tag_spec.rb
Expand Up @@ -14,7 +14,7 @@
describe ActsAsTaggableOn::Tag do
before(:each) do
@tag = ActsAsTaggableOn::Tag.new
@user = TaggableModel.create(name: 'Pablo')
@user = TaggableModel.create(name: 'Pablo', tenant_id: 100)
end


Expand Down Expand Up @@ -70,6 +70,21 @@
end
end

describe 'for tenant' do
before(:each) do
@user.skill_list.add('ruby')
@user.save
end

it 'should return tags for the tenant' do
expect(ActsAsTaggableOn::Tag.for_tenant('100').pluck(:name)).to include('ruby')
end

it 'should not return tags for other tenants' do
expect(ActsAsTaggableOn::Tag.for_tenant('200').pluck(:name)).to_not include('ruby')
end
end

describe 'find or create by name' do
before(:each) do
@tag.name = 'awesome'
Expand Down
8 changes: 6 additions & 2 deletions spec/acts_as_taggable_on/taggable_spec.rb
Expand Up @@ -109,6 +109,10 @@
expect(@taggable.tag_types).to eq(TaggableModel.tag_types)
end

it 'should have tenant column' do
expect(TaggableModel.tenant_column).to eq(:tenant_id)
end

it 'should have tag_counts_on' do
expect(TaggableModel.tag_counts_on(:tags)).to be_empty

Expand Down Expand Up @@ -676,11 +680,11 @@
end

it 'should return all column names joined for TaggableModel GROUP clause' do
expect(@taggable.grouped_column_names_for(TaggableModel)).to eq('taggable_models.id, taggable_models.name, taggable_models.type')
expect(@taggable.grouped_column_names_for(TaggableModel)).to eq('taggable_models.id, taggable_models.name, taggable_models.type, taggable_models.tenant_id')
end

it 'should return all column names joined for NonStandardIdTaggableModel GROUP clause' do
expect(@taggable.grouped_column_names_for(TaggableModel)).to eq("taggable_models.#{TaggableModel.primary_key}, taggable_models.name, taggable_models.type")
expect(@taggable.grouped_column_names_for(TaggableModel)).to eq("taggable_models.#{TaggableModel.primary_key}, taggable_models.name, taggable_models.type, taggable_models.tenant_id")
end
end

Expand Down
10 changes: 10 additions & 0 deletions spec/acts_as_taggable_on/tagging_spec.rb
Expand Up @@ -77,12 +77,14 @@
@tagging.tag = ActsAsTaggableOn::Tag.create(name: "Physics")
@tagging.tagger = @tagger
@tagging.context = 'Science'
@tagging.tenant = 'account1'
@tagging.save

@tagging_2.taggable = TaggableModel.create(name: "Satellites")
@tagging_2.tag = ActsAsTaggableOn::Tag.create(name: "Technology")
@tagging_2.tagger = @tagger_2
@tagging_2.context = 'Science'
@tagging_2.tenant = 'account1'
@tagging_2.save

@tagging_3.taggable = TaggableModel.create(name: "Satellites")
Expand Down Expand Up @@ -114,6 +116,14 @@
end
end

describe '.by_tenant' do
it "should find taggings by tenant" do
expect(ActsAsTaggableOn::Tagging.by_tenant('account1').length).to eq(2);
expect(ActsAsTaggableOn::Tagging.by_tenant('account1').first).to eq(@tagging);
expect(ActsAsTaggableOn::Tagging.by_tenant('account1').second).to eq(@tagging_2);
end
end

describe '.not_owned' do
before do
@tagging_4 = ActsAsTaggableOn::Tagging.new
Expand Down
2 changes: 2 additions & 0 deletions spec/internal/app/models/taggable_model.rb
Expand Up @@ -3,6 +3,8 @@ class TaggableModel < ActiveRecord::Base
acts_as_taggable_on :languages
acts_as_taggable_on :skills
acts_as_taggable_on :needs, :offerings
acts_as_taggable_tenant :tenant_id

has_many :untaggable_models

attr_reader :tag_list_submethod_called
Expand Down
3 changes: 3 additions & 0 deletions spec/internal/db/schema.rb
Expand Up @@ -21,6 +21,8 @@
# length for MyISAM table type: http://bit.ly/vgW2Ql
t.string :context, limit: 128

t.string :tenant , limit: 128

t.datetime :created_at
end
add_index ActsAsTaggableOn.taggings_table,
Expand All @@ -34,6 +36,7 @@
create_table :taggable_models, force: true do |t|
t.column :name, :string
t.column :type, :string
t.column :tenant_id, :integer
end

create_table :columns_override_models, force: true do |t|
Expand Down

0 comments on commit 7e696e3

Please sign in to comment.