Skip to content

Commit 7e696e3

Browse files
lunaruHeng
and
Heng
authored
Support tenants for taggings (mbleigh#1000)
* support tenants for taggings * Make tenant fully optional * Move section of README to the correct place Co-authored-by: Heng <[email protected]>
1 parent 98a0822 commit 7e696e3

File tree

11 files changed

+113
-4
lines changed

11 files changed

+113
-4
lines changed

README.md

+23
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ Review the generated migrations then migrate :
8080
rake db:migrate
8181
```
8282

83+
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.
84+
8385
#### For MySql users
8486
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:
8587

@@ -390,6 +392,27 @@ def remove_owned_tag
390392
end
391393
```
392394

395+
### Tag Tenancy
396+
397+
Tags support multi-tenancy. This is useful for applications where a Tag belongs to a scoped set of models:
398+
399+
```ruby
400+
class Account < ActiveRecord::Base
401+
has_many :photos
402+
end
403+
404+
class User < ActiveRecord::Base
405+
belongs_to :account
406+
acts_as_taggable_on :tags
407+
acts_as_taggable_tenant :account_id
408+
end
409+
410+
@user1.tag_list = ["foo", "bar"] # these taggings will automatically have the tenant saved
411+
@user2.tag_list = ["bar", "baz"]
412+
413+
ActsAsTaggableOn::Tag.for_tenant(@user1.account.id) # returns Tag models for "foo" and "bar", but not "baz"
414+
```
415+
393416
### Dirty objects
394417

395418
```ruby
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
if ActiveRecord.gem_version >= Gem::Version.new('5.0')
2+
class AddTenantToTaggings < ActiveRecord::Migration[4.2]; end
3+
else
4+
class AddTenantToTaggings < ActiveRecord::Migration; end
5+
end
6+
AddTenantToTaggings.class_eval do
7+
def self.up
8+
add_column :taggings, :tenant, :string, limit: 128
9+
add_index :taggings, :tenant unless index_exists? :taggings, :tenant
10+
end
11+
12+
def self.down
13+
remove_column :taggings, :tenant
14+
remove_index :taggings, :tenant
15+
end
16+
end

lib/acts_as_taggable_on/tag.rb

+6
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ def self.for_context(context)
5555
select("DISTINCT #{ActsAsTaggableOn.tags_table}.*")
5656
end
5757

58+
def self.for_tenant(tenant)
59+
joins(:taggings).
60+
where("#{ActsAsTaggableOn.taggings_table}.tenant = ?", tenant.to_s).
61+
select("DISTINCT #{ActsAsTaggableOn.tags_table}.*")
62+
end
63+
5864
### CLASS METHODS:
5965

6066
def self.find_or_create_with_like_by_name(name)

lib/acts_as_taggable_on/taggable.rb

+18
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,23 @@ def acts_as_ordered_taggable_on(*tag_types)
5454
taggable_on(true, tag_types)
5555
end
5656

57+
def acts_as_taggable_tenant(tenant)
58+
if taggable?
59+
self.tenant_column = tenant
60+
else
61+
class_attribute :tenant_column
62+
self.tenant_column = tenant
63+
end
64+
65+
# each of these add context-specific methods and must be
66+
# called on each call of taggable_on
67+
include Core
68+
include Collection
69+
include Cache
70+
include Ownership
71+
include Related
72+
end
73+
5774
private
5875

5976
# Make a model taggable on specified contexts
@@ -78,6 +95,7 @@ def taggable_on(preserve_tag_order, *tag_types)
7895
self.tag_types = tag_types
7996
class_attribute :preserve_tag_order
8097
self.preserve_tag_order = preserve_tag_order
98+
class_attribute :tenant_column
8199

82100
class_eval do
83101
has_many :taggings, as: :taggable, dependent: :destroy, class_name: '::ActsAsTaggableOn::Tagging'

lib/acts_as_taggable_on/taggable/core.rb

+11-1
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,12 @@ def tagging_contexts
214214
self.class.tag_types.map(&:to_s) + custom_contexts
215215
end
216216

217+
def tenant
218+
if self.class.tenant_column
219+
read_attribute(self.class.tenant_column)
220+
end
221+
end
222+
217223
def reload(*args)
218224
self.class.tag_types.each do |context|
219225
instance_variable_set("@#{context.to_s.singularize}_list", nil)
@@ -272,7 +278,11 @@ def save_tags
272278

273279
# Create new taggings:
274280
new_tags.each do |tag|
275-
taggings.create!(tag_id: tag.id, context: context.to_s, taggable: self)
281+
if tenant
282+
taggings.create!(tag_id: tag.id, context: context.to_s, taggable: self, tenant: tenant)
283+
else
284+
taggings.create!(tag_id: tag.id, context: context.to_s, taggable: self)
285+
end
276286
end
277287
end
278288

lib/acts_as_taggable_on/tagging.rb

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ class Tagging < ::ActiveRecord::Base #:nodoc:
1414
scope :by_contexts, ->(contexts) { where(context: (contexts || DEFAULT_CONTEXT)) }
1515
scope :by_context, ->(context = DEFAULT_CONTEXT) { by_contexts(context.to_s) }
1616

17+
scope :by_tenant, ->(tenant) { where(tenant: tenant) }
18+
1719
validates_presence_of :context
1820
validates_presence_of :tag_id
1921

spec/acts_as_taggable_on/tag_spec.rb

+16-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
describe ActsAsTaggableOn::Tag do
1515
before(:each) do
1616
@tag = ActsAsTaggableOn::Tag.new
17-
@user = TaggableModel.create(name: 'Pablo')
17+
@user = TaggableModel.create(name: 'Pablo', tenant_id: 100)
1818
end
1919

2020

@@ -70,6 +70,21 @@
7070
end
7171
end
7272

73+
describe 'for tenant' do
74+
before(:each) do
75+
@user.skill_list.add('ruby')
76+
@user.save
77+
end
78+
79+
it 'should return tags for the tenant' do
80+
expect(ActsAsTaggableOn::Tag.for_tenant('100').pluck(:name)).to include('ruby')
81+
end
82+
83+
it 'should not return tags for other tenants' do
84+
expect(ActsAsTaggableOn::Tag.for_tenant('200').pluck(:name)).to_not include('ruby')
85+
end
86+
end
87+
7388
describe 'find or create by name' do
7489
before(:each) do
7590
@tag.name = 'awesome'

spec/acts_as_taggable_on/taggable_spec.rb

+6-2
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@
109109
expect(@taggable.tag_types).to eq(TaggableModel.tag_types)
110110
end
111111

112+
it 'should have tenant column' do
113+
expect(TaggableModel.tenant_column).to eq(:tenant_id)
114+
end
115+
112116
it 'should have tag_counts_on' do
113117
expect(TaggableModel.tag_counts_on(:tags)).to be_empty
114118

@@ -676,11 +680,11 @@
676680
end
677681

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

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

spec/acts_as_taggable_on/tagging_spec.rb

+10
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,14 @@
7777
@tagging.tag = ActsAsTaggableOn::Tag.create(name: "Physics")
7878
@tagging.tagger = @tagger
7979
@tagging.context = 'Science'
80+
@tagging.tenant = 'account1'
8081
@tagging.save
8182

8283
@tagging_2.taggable = TaggableModel.create(name: "Satellites")
8384
@tagging_2.tag = ActsAsTaggableOn::Tag.create(name: "Technology")
8485
@tagging_2.tagger = @tagger_2
8586
@tagging_2.context = 'Science'
87+
@tagging_2.tenant = 'account1'
8688
@tagging_2.save
8789

8890
@tagging_3.taggable = TaggableModel.create(name: "Satellites")
@@ -114,6 +116,14 @@
114116
end
115117
end
116118

119+
describe '.by_tenant' do
120+
it "should find taggings by tenant" do
121+
expect(ActsAsTaggableOn::Tagging.by_tenant('account1').length).to eq(2);
122+
expect(ActsAsTaggableOn::Tagging.by_tenant('account1').first).to eq(@tagging);
123+
expect(ActsAsTaggableOn::Tagging.by_tenant('account1').second).to eq(@tagging_2);
124+
end
125+
end
126+
117127
describe '.not_owned' do
118128
before do
119129
@tagging_4 = ActsAsTaggableOn::Tagging.new

spec/internal/app/models/taggable_model.rb

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ class TaggableModel < ActiveRecord::Base
33
acts_as_taggable_on :languages
44
acts_as_taggable_on :skills
55
acts_as_taggable_on :needs, :offerings
6+
acts_as_taggable_tenant :tenant_id
7+
68
has_many :untaggable_models
79

810
attr_reader :tag_list_submethod_called

spec/internal/db/schema.rb

+3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
# length for MyISAM table type: http://bit.ly/vgW2Ql
2222
t.string :context, limit: 128
2323

24+
t.string :tenant , limit: 128
25+
2426
t.datetime :created_at
2527
end
2628
add_index ActsAsTaggableOn.taggings_table,
@@ -34,6 +36,7 @@
3436
create_table :taggable_models, force: true do |t|
3537
t.column :name, :string
3638
t.column :type, :string
39+
t.column :tenant_id, :integer
3740
end
3841

3942
create_table :columns_override_models, force: true do |t|

0 commit comments

Comments
 (0)