acts_as_tenant 0.2
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.
- data/.gitignore +6 -0
- data/.rvmrc +1 -0
- data/Gemfile +4 -0
- data/MIT-LICENSE +20 -0
- data/README.md +117 -0
- data/Rakefile +1 -0
- data/acts_as_tenant.gemspec +20 -0
- data/lib/acts_as_tenant.rb +22 -0
- data/lib/acts_as_tenant/controller_extensions.rb +52 -0
- data/lib/acts_as_tenant/model_extensions.rb +89 -0
- data/lib/acts_as_tenant/version.rb +3 -0
- data/rails/init.rb +2 -0
- data/spec/database.yml +3 -0
- data/spec/model_extensions_spec.rb +178 -0
- data/spec/spec_helper.rb +41 -0
- metadata +64 -0
data/.gitignore
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm 1.9.2@rails31
|
data/Gemfile
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 [name of plugin creator]
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
Acts As Tenant
|
2
|
+
==============
|
3
|
+
|
4
|
+
note: acts_as_tenant was introduced in this [blog post](http://www.rollcallapp.com/blog/add).
|
5
|
+
|
6
|
+
This gem was born out of our own need for a fail-safe and out-of-the-way manner to add multi-tenancy to our Rails app through a shared database strategy, that integrates (near) seamless with Rails.
|
7
|
+
|
8
|
+
acts_as_tenant adds the ability to scope models to a tenant. Tenants are represented by a tenant model, such as `Account`. acts_as_tenant will help you set the current tenant on each request and ensures all 'tenant models' are always properly scoped to the current tenant: when viewing, searching and creating.
|
9
|
+
|
10
|
+
In addition, acts_as_tenant:
|
11
|
+
|
12
|
+
* sets the current tenant using the subdomain or allows you to pass in the current tenant yourself
|
13
|
+
* protects against various types of nastiness directed at circumventing the tenant scoping
|
14
|
+
* adds a method to validate uniqueness to a tenant, validates_uniqueness_to_tenant
|
15
|
+
* sets up a helper method containing the current tenant
|
16
|
+
|
17
|
+
Installation
|
18
|
+
------------
|
19
|
+
acts_as_tenant will only work on Rails 3.1 and up. This is due to changes made to the handling of default_scope, an essential pillar of the gem.
|
20
|
+
|
21
|
+
To use it, add it to your Gemfile:
|
22
|
+
|
23
|
+
gem 'acts_as_tenant'
|
24
|
+
|
25
|
+
Getting started
|
26
|
+
===============
|
27
|
+
There are two steps in adding multi-tenancy to your app with acts_as_tenant:
|
28
|
+
|
29
|
+
1. setting the current tenant and
|
30
|
+
2. scoping your models.
|
31
|
+
|
32
|
+
Setting the current tenant
|
33
|
+
--------------------------
|
34
|
+
There are two ways to set the current tenant: (1) by using the subdomain to lookup the current tenant and (2) by passing in the current tenant yourself.
|
35
|
+
|
36
|
+
**Use the subdomain to lookup the current tenant**
|
37
|
+
|
38
|
+
class ApplicationController < ActionController::Base
|
39
|
+
set_current_tenant_by_subdomain(:account, :subdomain)
|
40
|
+
end
|
41
|
+
This tells acts_as_tenant to use the current subdomain to identify the current tenant. In addition, it tells acts_as_tenant that tenants are represented by the Account model and this model has a column named 'subdomain' which can be used to lookup the Account using the actual subdomain. If ommitted, the parameters will default to the values used above.
|
42
|
+
|
43
|
+
**OR Pass in the current tenant yourself**
|
44
|
+
|
45
|
+
class ApplicationController < ActionController::Base
|
46
|
+
current_account = Account.find_the_current_account
|
47
|
+
set_current_tenant_to(current_account)
|
48
|
+
end
|
49
|
+
This allows you to pass in the current tenant yourself.
|
50
|
+
|
51
|
+
**note:** If the current tenant is not set by either of these methods, Acts_as_tenant will be unable to apply the proper scope to your models. So make sure you use one of the two methods to tell acts_as_tenant about the current tenant.
|
52
|
+
|
53
|
+
Scoping your models
|
54
|
+
-------------------
|
55
|
+
class Addaccounttousers < ActiveRecord::Migration
|
56
|
+
def up
|
57
|
+
add_column :users, :account_id, :integer
|
58
|
+
end
|
59
|
+
|
60
|
+
class User < ActiveRecord::Base
|
61
|
+
acts_as_tenant(:account)
|
62
|
+
end
|
63
|
+
|
64
|
+
acts_as_tenant requires each scoped model to have a column in its schema linking it to a tenant. Adding acts_as_tenant to your model declaration will scope that model to the current tenant **BUT ONLY if a current tenant has been set**.
|
65
|
+
|
66
|
+
Some examples to illustrate this behavior:
|
67
|
+
|
68
|
+
# This manually sets the current tenant for testing purposes. In your app this is handled by the gem.
|
69
|
+
acts_as_tenant.current_tenant = Account.find(3)
|
70
|
+
|
71
|
+
# All searches are scoped by the tenant, the following searches will only return objects
|
72
|
+
# where account_id == 3
|
73
|
+
Project.all => # all projects with account_id => 3
|
74
|
+
Project.tasks.all # => all tasks with account_id => 3
|
75
|
+
|
76
|
+
# New objects are scoped to the current tenant
|
77
|
+
@project = Project.new(:name => 'big project') # => <#Project id: nil, name: 'big project', :account_id: 3>
|
78
|
+
|
79
|
+
# It will not allow the creation of objects outside the current_tenant scope
|
80
|
+
@project.account_id = 2
|
81
|
+
@project.save # => false
|
82
|
+
|
83
|
+
# It will not allow association with objects outside the current tenant scope
|
84
|
+
# Assuming the Project with ID: 2 does not belong to Account with ID: 3
|
85
|
+
@task = Task.new # => <#Task id: nil, name: bil, project_id: nil, :account_id: 3>
|
86
|
+
|
87
|
+
Acts_as_tenant uses Rails' default_scope method to scope models. Rails 3.1 changed the way default_scope works in a good way. A user defined default_scope should integrate seamlessly with the one added by acts_as_tenant.
|
88
|
+
|
89
|
+
**Validating attribute uniqueness**
|
90
|
+
If you need to validate for uniqueness, chances are that you want to scope this validation to a tenant. You can do so by using:
|
91
|
+
|
92
|
+
validates_uniqueness_to_tenant :name, :email
|
93
|
+
|
94
|
+
All options available to Rails' own @validates_uniqueness_of@ are also available to this method.
|
95
|
+
|
96
|
+
To Do
|
97
|
+
-----
|
98
|
+
* Change the tests to Test::Unit so I can easily add some controller tests.
|
99
|
+
|
100
|
+
Bug reports & suggested improvements
|
101
|
+
------------------------------------
|
102
|
+
If you have found a bug or want to suggest an improvement, please use our issue tracked at:
|
103
|
+
|
104
|
+
[github.com/ErwinM/acts_as_tenant/issues](http://github.com/ErwinM/acts_as_tenant/issues)
|
105
|
+
|
106
|
+
If you want to contribute, fork the project, code your improvements and make a pull request on [Github](http://github.com/ErwinM/acts_as_tenant/). When doing so, please don't forget to add tests. If your contribution is fixing a bug it would be perfect if you could also submit a failing test, illustrating the issue.
|
107
|
+
|
108
|
+
Author & Credits
|
109
|
+
----------------
|
110
|
+
acts_as_tenant is written by Erwin Matthijssen.
|
111
|
+
Erwin is currently busy developing [Roll Call](http://www.rollcallapp.com/ "Roll Call App").
|
112
|
+
|
113
|
+
This gem was inspired by Ryan Sonnek's [Multitenant](https://github.com/wireframe/multitenant) gem and its use of default_scope.
|
114
|
+
|
115
|
+
License
|
116
|
+
-------
|
117
|
+
Copyright (c) 2011 Erwin Matthijssen, released under the MIT license
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "acts_as_tenant/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "acts_as_tenant"
|
7
|
+
s.version = ActsAsTenant::VERSION
|
8
|
+
s.authors = ["Erwin Matthijssen"]
|
9
|
+
s.email = ["erwin.matthijssen@gmail.com"]
|
10
|
+
s.homepage = "http://www.rollcallapp.com/blog"
|
11
|
+
s.summary = %q{Add multi-tenancy to Rails applications using a shared db strategy}
|
12
|
+
s.description = %q{Integrates multi-tenancy into a Rails application in a convenient and out-of-your way manner}
|
13
|
+
|
14
|
+
s.rubyforge_project = "acts_as_tenant"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
#RAILS_3 = ::ActiveRecord::VERSION::MAJOR >= 3
|
2
|
+
|
3
|
+
require "active_record"
|
4
|
+
require "action_controller"
|
5
|
+
require "active_model"
|
6
|
+
|
7
|
+
#$LOAD_PATH.unshift(File.dirname(__FILE__))
|
8
|
+
|
9
|
+
require "acts_as_tenant"
|
10
|
+
require "acts_as_tenant/version"
|
11
|
+
require "acts_as_tenant/controller_extensions.rb"
|
12
|
+
require "acts_as_tenant/model_extensions.rb"
|
13
|
+
|
14
|
+
#$LOAD_PATH.shift
|
15
|
+
|
16
|
+
if defined?(ActiveRecord::Base)
|
17
|
+
ActiveRecord::Base.send(:include, ActsAsTenant::ModelExtensions)
|
18
|
+
ActionController::Base.extend ActsAsTenant::ControllerExtensions
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module ActsAsTenant
|
2
|
+
module ControllerExtensions
|
3
|
+
|
4
|
+
# this method allows setting the current_tenant by reading the subdomain and looking
|
5
|
+
# it up in the tenant-model passed to the method (defaults to Account). The method will
|
6
|
+
# look for the subdomain in a column referenced by the second argument (defaults to subdomain).
|
7
|
+
def set_current_tenant_by_subdomain(tenant = :account, column = :subdomain )
|
8
|
+
self.class_eval do
|
9
|
+
cattr_accessor :tenant_class, :tenant_column
|
10
|
+
attr_accessor :current_tenant
|
11
|
+
end
|
12
|
+
|
13
|
+
self.tenant_class = tenant.to_s.capitalize.constantize
|
14
|
+
self.tenant_column = column.to_sym
|
15
|
+
|
16
|
+
self.class_eval do
|
17
|
+
before_filter :find_tenant_by_subdomain
|
18
|
+
|
19
|
+
helper_method :current_tenant
|
20
|
+
|
21
|
+
private
|
22
|
+
def find_tenant_by_subdomain
|
23
|
+
ActsAsTenant.current_tenant = tenant_class.where(tenant_column => request.subdomains.first).first
|
24
|
+
@current_tenant_instance = ActsAsTenant.current_tenant
|
25
|
+
end
|
26
|
+
|
27
|
+
# helper method to have the current_tenant available in the controller
|
28
|
+
def current_tenant
|
29
|
+
ActsAsTenant.current_tenant
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# this method allows manual setting of the current_tenant by passing in a tenant object
|
35
|
+
#
|
36
|
+
def set_current_tenant_to(current_tenant_object)
|
37
|
+
self.class_eval do
|
38
|
+
cattr_accessor :tenant_class
|
39
|
+
attr_accessor :current_tenant
|
40
|
+
before_filter lambda { @current_tenant_instance = ActsAsTenant.current_tenant = current_tenant_object }
|
41
|
+
|
42
|
+
helper_method :current_tenant
|
43
|
+
|
44
|
+
private
|
45
|
+
# helper method to have the current_tenant available in the controller
|
46
|
+
def current_tenant
|
47
|
+
ActsAsTenant.current_tenant
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# ActsAsTenant
|
2
|
+
|
3
|
+
|
4
|
+
module ActsAsTenant
|
5
|
+
|
6
|
+
class << self
|
7
|
+
cattr_accessor :tenant_class
|
8
|
+
attr_accessor :current_tenant
|
9
|
+
end
|
10
|
+
|
11
|
+
module ModelExtensions
|
12
|
+
extend ActiveSupport::Concern
|
13
|
+
|
14
|
+
# Alias the v_uniqueness_of method so we can scope it to the current tenant when relevant
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
|
18
|
+
def acts_as_tenant(association = :account)
|
19
|
+
|
20
|
+
# Method that enables checking if a class is scoped by tenant
|
21
|
+
def self.is_scoped_by_tenant?
|
22
|
+
true
|
23
|
+
end
|
24
|
+
|
25
|
+
ActsAsTenant.tenant_class ||= association
|
26
|
+
|
27
|
+
# Setup the association between the class and the tenant class
|
28
|
+
belongs_to association
|
29
|
+
|
30
|
+
# get the tenant model and its foreign key
|
31
|
+
reflection = reflect_on_association association
|
32
|
+
|
33
|
+
# As the "foreign_key" method changed name in 3.1 we check for backward compatibility
|
34
|
+
if reflection.respond_to?(:foreign_key)
|
35
|
+
fkey = reflection.foreign_key
|
36
|
+
else
|
37
|
+
fkey = reflection.association_foreign_key
|
38
|
+
end
|
39
|
+
|
40
|
+
# set the current_tenant on newly created objects
|
41
|
+
before_validation Proc.new {|m|
|
42
|
+
return unless ActsAsTenant.current_tenant
|
43
|
+
m.send "#{association}=".to_sym, ActsAsTenant.current_tenant
|
44
|
+
}, :on => :create
|
45
|
+
|
46
|
+
# set the default_scope to scope to current tenant
|
47
|
+
default_scope lambda {
|
48
|
+
where({fkey => ActsAsTenant.current_tenant.id}) if ActsAsTenant.current_tenant
|
49
|
+
}
|
50
|
+
|
51
|
+
# Rewrite accessors to make tenant foreign_key/association immutable
|
52
|
+
define_method "#{fkey}=" do |integer|
|
53
|
+
if new_record?
|
54
|
+
write_attribute(fkey, integer)
|
55
|
+
else
|
56
|
+
raise "#{fkey} is immutable!"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
define_method "#{association}=" do |model|
|
61
|
+
if new_record?
|
62
|
+
write_attribute(association, model)
|
63
|
+
else
|
64
|
+
raise "#{association} is immutable!"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# add validation of associations against tenant scope
|
69
|
+
# we can't do this for polymorphic associations so we
|
70
|
+
# exempt them
|
71
|
+
reflect_on_all_associations.each do |a|
|
72
|
+
unless a == reflection || a.macro == :has_many || a.options[:polymorphic]
|
73
|
+
validates_each a.foreign_key.to_sym do |record, attr, value|
|
74
|
+
record.errors.add attr, "is invalid" unless a.name.to_s.classify.constantize.where(:id => value).present?
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def validates_uniqueness_to_tenant(fields, args ={})
|
81
|
+
raise "ActsAsTenant::validates_uniqueness_to_tenant: no current tenant" unless respond_to?(:is_scoped_by_tenant?)
|
82
|
+
tenant_id = lambda { "#{ActsAsTenant.tenant_class.to_s.downcase}_id"}.call
|
83
|
+
args[:scope].nil? ? args[:scope] = tenant_id : args[:scope] << tenant_id
|
84
|
+
validates_uniqueness_of(fields, args)
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
data/rails/init.rb
ADDED
data/spec/database.yml
ADDED
@@ -0,0 +1,178 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
# Setup the db
|
4
|
+
ActiveRecord::Schema.define(:version => 1) do
|
5
|
+
create_table :accounts, :force => true do |t|
|
6
|
+
t.column :name, :string
|
7
|
+
end
|
8
|
+
|
9
|
+
create_table :projects, :force => true do |t|
|
10
|
+
t.column :name, :string
|
11
|
+
t.column :account_id, :integer
|
12
|
+
end
|
13
|
+
|
14
|
+
create_table :tasks, :force => true do |t|
|
15
|
+
t.column :name, :string
|
16
|
+
t.column :account_id, :integer
|
17
|
+
t.column :project_id, :integer
|
18
|
+
t.column :completed, :boolean
|
19
|
+
end
|
20
|
+
|
21
|
+
create_table :countries, :force => true do |t|
|
22
|
+
t.column :name, :string
|
23
|
+
end
|
24
|
+
|
25
|
+
create_table :cities, :force => true do |t|
|
26
|
+
t.column :name, :string
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
# Setup the models
|
32
|
+
class Account < ActiveRecord::Base
|
33
|
+
has_many :projects
|
34
|
+
end
|
35
|
+
|
36
|
+
class Project < ActiveRecord::Base
|
37
|
+
has_many :tasks
|
38
|
+
acts_as_tenant :account
|
39
|
+
|
40
|
+
validates_uniqueness_to_tenant :name
|
41
|
+
end
|
42
|
+
|
43
|
+
class Task < ActiveRecord::Base
|
44
|
+
belongs_to :project
|
45
|
+
default_scope :conditions => { :completed => nil }, :order => "name"
|
46
|
+
|
47
|
+
acts_as_tenant :account
|
48
|
+
validates_uniqueness_of :name
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
class City < ActiveRecord::Base
|
53
|
+
validates_uniqueness_of :name
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
# Start testing!
|
58
|
+
describe ActsAsTenant do
|
59
|
+
after { ActsAsTenant.current_tenant = nil }
|
60
|
+
|
61
|
+
describe 'Setting the current tenant' do
|
62
|
+
before { ActsAsTenant.current_tenant = :foo }
|
63
|
+
it { ActsAsTenant.current_tenant == :foo }
|
64
|
+
end
|
65
|
+
|
66
|
+
describe 'is_scoped_as_tenant should return the correct value' do
|
67
|
+
it {Project.respond_to?(:is_scoped_by_tenant?).should == true}
|
68
|
+
end
|
69
|
+
|
70
|
+
describe 'Project.all should be scoped to the current tenant if set' do
|
71
|
+
before do
|
72
|
+
@account1 = Account.create!(:name => 'foo')
|
73
|
+
@account2 = Account.create!(:name => 'bar')
|
74
|
+
|
75
|
+
@project1 = @account1.projects.create!(:name => 'foobar')
|
76
|
+
@project2 = @account2.projects.create!(:name => 'baz')
|
77
|
+
|
78
|
+
ActsAsTenant.current_tenant= @account1
|
79
|
+
@projects = Project.all
|
80
|
+
end
|
81
|
+
|
82
|
+
it { @projects.length.should == 1 }
|
83
|
+
it { @projects.should == [@project1] }
|
84
|
+
end
|
85
|
+
|
86
|
+
describe 'Associations should be correctly scoped by current tenant' do
|
87
|
+
before do
|
88
|
+
@account = Account.create!(:name => 'foo')
|
89
|
+
@project = @account.projects.create!(:name => 'foobar', :account_id => @account.id )
|
90
|
+
# the next line would normally be nearly impossible: a task assigned to a tenant project,
|
91
|
+
# but the task has no tenant assigned
|
92
|
+
@task1 = Task.create!(:name => 'no_tenant', :project => @project)
|
93
|
+
|
94
|
+
ActsAsTenant.current_tenant = @account
|
95
|
+
@task2 = @project.tasks.create!(:name => 'baz')
|
96
|
+
@tasks = @project.tasks
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'should correctly set the tenant on the task created with current_tenant set' do
|
100
|
+
@task2.account.should == @account
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'should filter out the non-tenant task from the project' do
|
104
|
+
@tasks.length.should == 1
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
describe 'When dealing with a user defined default_scope' do
|
109
|
+
before do
|
110
|
+
@account = Account.create!(:name => 'foo')
|
111
|
+
@project1 = Project.create!(:name => 'inaccessible')
|
112
|
+
@task1 = Task.create!(:name => 'no_tenant', :project => @project1)
|
113
|
+
|
114
|
+
ActsAsTenant.current_tenant = @account
|
115
|
+
@project2 = Project.create!(:name => 'accessible')
|
116
|
+
@task2 = @project2.tasks.create!(:name => 'bar')
|
117
|
+
@task3 = @project2.tasks.create!(:name => 'baz')
|
118
|
+
@task4 = @project2.tasks.create!(:name => 'foo')
|
119
|
+
@task5 = @project2.tasks.create!(:name => 'foobar', :completed => true )
|
120
|
+
|
121
|
+
@tasks= Task.all
|
122
|
+
end
|
123
|
+
|
124
|
+
it 'should apply both the tenant scope and the user defined default_scope, including :order' do
|
125
|
+
@tasks.length.should == 3
|
126
|
+
@tasks.should == [@task2, @task3, @task4]
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
describe 'tenant_id should be immutable' do
|
131
|
+
before do
|
132
|
+
@account = Account.create!(:name => 'foo')
|
133
|
+
@project = @account.projects.create!(:name => 'bar')
|
134
|
+
end
|
135
|
+
|
136
|
+
it { lambda {@project.account_id = @account.id + 1}.should raise_error }
|
137
|
+
end
|
138
|
+
|
139
|
+
describe 'Associations can only be made with in-scope objects' do
|
140
|
+
before do
|
141
|
+
@account = Account.create!(:name => 'foo')
|
142
|
+
@project1 = Project.create!(:name => 'inaccessible_project', :account_id => @account.id + 1)
|
143
|
+
|
144
|
+
ActsAsTenant.current_tenant = @account
|
145
|
+
@project2 = Project.create!(:name => 'accessible_project')
|
146
|
+
@task = @project2.tasks.create!(:name => 'bar')
|
147
|
+
end
|
148
|
+
|
149
|
+
it { @task.update_attributes(:project_id => @project1.id).should == false }
|
150
|
+
end
|
151
|
+
|
152
|
+
describe 'When using validates_uniqueness_to_tenant in a aat model' do
|
153
|
+
before do
|
154
|
+
@account = Account.create!(:name => 'foo')
|
155
|
+
ActsAsTenant.current_tenant = @account
|
156
|
+
@project1 = Project.create!(:name => 'bar')
|
157
|
+
end
|
158
|
+
|
159
|
+
it 'should not be possible to create a duplicate within the same tenant' do
|
160
|
+
@project2 = Project.create(:name => 'bar').valid?.should == false
|
161
|
+
end
|
162
|
+
|
163
|
+
it 'should be possible to create a duplicate outside the tenant scope' do
|
164
|
+
@account = Account.create!(:name => 'baz')
|
165
|
+
ActsAsTenant.current_tenant = @account
|
166
|
+
@project2 = Project.create(:name => 'bar').valid?.should == true
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
describe 'When using validates_uniqueness_of in a NON-aat model' do
|
171
|
+
before do
|
172
|
+
@city1 = City.create!(:name => 'foo')
|
173
|
+
end
|
174
|
+
it 'should not be possible to create duplicates' do
|
175
|
+
@city2 = City.create(:name => 'foo').valid?.should == false
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
2
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
3
|
+
require 'rspec'
|
4
|
+
require 'active_record'
|
5
|
+
require 'action_controller'
|
6
|
+
require 'logger'
|
7
|
+
require 'database_cleaner'
|
8
|
+
|
9
|
+
require 'acts_as_tenant/model_extensions'
|
10
|
+
require 'acts_as_tenant/controller_extensions'
|
11
|
+
|
12
|
+
ActiveRecord::Base.send(:include, ActsAsTenant::ModelExtensions)
|
13
|
+
ActionController::Base.extend ActsAsTenant::ControllerExtensions
|
14
|
+
|
15
|
+
config = YAML::load(IO.read(File.join(File.dirname(__FILE__), 'database.yml')))
|
16
|
+
ActiveRecord::Base.logger = Logger.new(File.join(File.dirname(__FILE__), "debug.log"))
|
17
|
+
ActiveRecord::Base.establish_connection(config[ENV['DB'] || 'sqlite'])
|
18
|
+
|
19
|
+
# Requires supporting files with custom matchers and macros, etc,
|
20
|
+
# in ./support/ and its subdirectories.
|
21
|
+
#Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
|
22
|
+
|
23
|
+
#RSpec.configure do |config|
|
24
|
+
#end
|
25
|
+
|
26
|
+
Spec::Runner.configure do |config|
|
27
|
+
|
28
|
+
config.before(:suite) do
|
29
|
+
DatabaseCleaner.strategy = :transaction
|
30
|
+
DatabaseCleaner.clean_with(:truncation)
|
31
|
+
end
|
32
|
+
|
33
|
+
config.before(:each) do
|
34
|
+
DatabaseCleaner.start
|
35
|
+
end
|
36
|
+
|
37
|
+
config.after(:each) do
|
38
|
+
DatabaseCleaner.clean
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
metadata
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: acts_as_tenant
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.2'
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Erwin Matthijssen
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-10-03 00:00:00.000000000Z
|
13
|
+
dependencies: []
|
14
|
+
description: Integrates multi-tenancy into a Rails application in a convenient and
|
15
|
+
out-of-your way manner
|
16
|
+
email:
|
17
|
+
- erwin.matthijssen@gmail.com
|
18
|
+
executables: []
|
19
|
+
extensions: []
|
20
|
+
extra_rdoc_files: []
|
21
|
+
files:
|
22
|
+
- .gitignore
|
23
|
+
- .rvmrc
|
24
|
+
- Gemfile
|
25
|
+
- MIT-LICENSE
|
26
|
+
- README.md
|
27
|
+
- Rakefile
|
28
|
+
- acts_as_tenant.gemspec
|
29
|
+
- lib/acts_as_tenant.rb
|
30
|
+
- lib/acts_as_tenant/controller_extensions.rb
|
31
|
+
- lib/acts_as_tenant/model_extensions.rb
|
32
|
+
- lib/acts_as_tenant/version.rb
|
33
|
+
- rails/init.rb
|
34
|
+
- spec/database.yml
|
35
|
+
- spec/model_extensions_spec.rb
|
36
|
+
- spec/spec_helper.rb
|
37
|
+
homepage: http://www.rollcallapp.com/blog
|
38
|
+
licenses: []
|
39
|
+
post_install_message:
|
40
|
+
rdoc_options: []
|
41
|
+
require_paths:
|
42
|
+
- lib
|
43
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
44
|
+
none: false
|
45
|
+
requirements:
|
46
|
+
- - ! '>='
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
requirements: []
|
56
|
+
rubyforge_project: acts_as_tenant
|
57
|
+
rubygems_version: 1.8.6
|
58
|
+
signing_key:
|
59
|
+
specification_version: 3
|
60
|
+
summary: Add multi-tenancy to Rails applications using a shared db strategy
|
61
|
+
test_files:
|
62
|
+
- spec/database.yml
|
63
|
+
- spec/model_extensions_spec.rb
|
64
|
+
- spec/spec_helper.rb
|