tenancy 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.gitignore +18 -0
- data/.rspec +1 -0
- data/.rvmrc +80 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +146 -0
- data/Rakefile +1 -0
- data/lib/tenancy.rb +7 -0
- data/lib/tenancy/matchers.rb +62 -0
- data/lib/tenancy/resource.rb +42 -0
- data/lib/tenancy/resource_scope.rb +52 -0
- data/lib/tenancy/version.rb +3 -0
- data/spec/lib/resource_scope_spec.rb +107 -0
- data/spec/lib/resource_spec.rb +57 -0
- data/spec/lib/shoulda_matchers_spec.rb +12 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/support/models.rb +25 -0
- data/spec/support/schema.rb +36 -0
- data/tenancy.gemspec +28 -0
- metadata +153 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
OGQ1N2M3MjhhMTIwNzUwNDg5MjM3YjdmZTAzNmU0MjViYjk1ZjhjYQ==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
MzUyY2NjNjA0YWRlOGY3M2QzZjY5ODI1Y2I5ZGU4ODMyYmI4YzljZA==
|
7
|
+
!binary "U0hBNTEy":
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
YjM4NWFhMjEwZTAxMGZhNTRmZTcyMWYwYzQ2Njk2ZjNkZTZlMDg3YjRjYTRi
|
10
|
+
ZWY2OTg3ODliYzQ5YzQwMjNkYmRlNzc4MTZmMTdlZTI0OTAzNjI2ZGQ3MmIz
|
11
|
+
NzUxNjFjYzc3NDc1MmE0NzY4Mzg0YTUxZDYyNWE3ZDU5MDkyNDY=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
MGI2MWY0ZjI2NjM1ZDcyMWRhMTAwNDhkMzIxOTIwNGQ2YmYzYmM4NmNlZjk5
|
14
|
+
YzE1MWJhNDUwZTAyNjY4M2M3YzViNjEyYzI0MzU4YTlmMzRiYmExZGJkMzRh
|
15
|
+
MTQ4MWI3NWQ3MzhmNDQwZjBmMGY0YjE1MTc2Y2QyNTQwNDQ2YTc=
|
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--colour
|
data/.rvmrc
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
#!/usr/bin/env bash
|
2
|
+
|
3
|
+
# This is an RVM Project .rvmrc file, used to automatically load the ruby
|
4
|
+
# development environment upon cd'ing into the directory
|
5
|
+
|
6
|
+
# First we specify our desired <ruby>[@<gemset>], the @gemset name is optional.
|
7
|
+
environment_id="ruby-2.0.0-p247@tenancy"
|
8
|
+
|
9
|
+
#
|
10
|
+
# Uncomment the following lines if you want to verify rvm version per project
|
11
|
+
#
|
12
|
+
rvmrc_rvm_version="1.17.10" # 1.10.1 seams as a safe start
|
13
|
+
eval "$(echo ${rvm_version}.${rvmrc_rvm_version} | awk -F. '{print "[[ "$1*65536+$2*256+$3" -ge "$4*65536+$5*256+$6" ]]"}' )" || {
|
14
|
+
echo "This .rvmrc file requires at least RVM ${rvmrc_rvm_version}, aborting loading."
|
15
|
+
return 1
|
16
|
+
}
|
17
|
+
#
|
18
|
+
|
19
|
+
#
|
20
|
+
# Uncomment following line if you want options to be set only for given project.
|
21
|
+
#
|
22
|
+
# PROJECT_JRUBY_OPTS=( --1.9 )
|
23
|
+
#
|
24
|
+
# The variable PROJECT_JRUBY_OPTS requires the following to be run in shell:
|
25
|
+
#
|
26
|
+
# chmod +x ${rvm_path}/hooks/after_use_jruby_opts
|
27
|
+
#
|
28
|
+
|
29
|
+
#
|
30
|
+
# First we attempt to load the desired environment directly from the environment
|
31
|
+
# file. This is very fast and efficient compared to running through the entire
|
32
|
+
# CLI and selector. If you want feedback on which environment was used then
|
33
|
+
# insert the word 'use' after --create as this triggers verbose mode.
|
34
|
+
#
|
35
|
+
if [[ -d "${rvm_path:-$HOME/.rvm}/environments" \
|
36
|
+
&& -s "${rvm_path:-$HOME/.rvm}/environments/$environment_id" ]]
|
37
|
+
then
|
38
|
+
\. "${rvm_path:-$HOME/.rvm}/environments/$environment_id"
|
39
|
+
|
40
|
+
if [[ -s "${rvm_path:-$HOME/.rvm}/hooks/after_use" ]]
|
41
|
+
then
|
42
|
+
. "${rvm_path:-$HOME/.rvm}/hooks/after_use"
|
43
|
+
fi
|
44
|
+
else
|
45
|
+
# If the environment file has not yet been created, use the RVM CLI to select.
|
46
|
+
if ! rvm --create use "$environment_id"
|
47
|
+
then
|
48
|
+
echo "Failed to create RVM environment '${environment_id}'."
|
49
|
+
return 1
|
50
|
+
fi
|
51
|
+
fi
|
52
|
+
|
53
|
+
#
|
54
|
+
# If you use an RVM gemset file to install a list of gems (*.gems), you can have
|
55
|
+
# it be automatically loaded. Uncomment the following and adjust the filename if
|
56
|
+
# necessary.
|
57
|
+
#
|
58
|
+
# filename=".gems"
|
59
|
+
# if [[ -s "$filename" ]]
|
60
|
+
# then
|
61
|
+
# rvm gemset import "$filename" | grep -v already | grep -v listed | grep -v complete | sed '/^$/d'
|
62
|
+
# fi
|
63
|
+
|
64
|
+
# If you use bundler, this might be useful to you:
|
65
|
+
if [[ -s Gemfile ]] && ! command -v bundle >/dev/null
|
66
|
+
then
|
67
|
+
printf "%b" "The rubygem 'bundler' is not installed. Installing it now.\n"
|
68
|
+
gem install bundler
|
69
|
+
fi
|
70
|
+
if [[ -s Gemfile ]] && command -v bundle
|
71
|
+
then
|
72
|
+
bundle install
|
73
|
+
fi
|
74
|
+
|
75
|
+
if [[ $- == *i* ]] # check for interactive shells
|
76
|
+
then
|
77
|
+
echo "Using: $(tput setaf 2)$GEM_HOME$(tput sgr0)" # show the user the ruby and gemset they are using in green
|
78
|
+
else
|
79
|
+
echo "Using: $GEM_HOME" # don't use colors in interactive shells
|
80
|
+
fi
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 chamnap
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
# Tenancy
|
2
|
+
|
3
|
+
`tenancy` is a simple gem that provides multi-tenancy support on activerecord through scoping. I suggest you to watch an excellent [RailsCast on Multitenancy with Scopes](http://railscasts.com/episodes/388-multitenancy-with-scopes) and read this book [Multitenancy with Rails](https://leanpub.com/multi-tenancy-rails).
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'tenancy'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
```
|
16
|
+
$ bundle
|
17
|
+
```
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
This gem provides two modules: `Tenancy::Resource` and `Tenancy::ResourceScope`.
|
22
|
+
|
23
|
+
### Tenancy::Resource
|
24
|
+
|
25
|
+
`Tenancy::Resource` is a module which you want others to be scoped by.
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
class Portal < ActiveRecord::Base
|
29
|
+
include Tenancy::Resource
|
30
|
+
end
|
31
|
+
|
32
|
+
camyp = Portal.where(domain_name: 'yp.com.kh').first
|
33
|
+
# => <Portal id: 1, domain_name: 'yp.com.kh'>
|
34
|
+
|
35
|
+
# set current portal by id
|
36
|
+
Portal.current = camyp
|
37
|
+
|
38
|
+
# or portal object
|
39
|
+
Portal.current = 1
|
40
|
+
|
41
|
+
# get current portal
|
42
|
+
Portal.current
|
43
|
+
# => <Portal id: 1, domain_name: 'yp.com.kh'>
|
44
|
+
|
45
|
+
# scope with this portal
|
46
|
+
Portal.with(camyp) do
|
47
|
+
# Do something here with this portal
|
48
|
+
end
|
49
|
+
```
|
50
|
+
|
51
|
+
### Tenancy::ResourceScope
|
52
|
+
|
53
|
+
`Tenancy::ResourceScope` is a module which you want to scope itself to `Tenancy::Resource`.
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
class Listing < ActiveRecord::Base
|
57
|
+
include Tenancy::Resource
|
58
|
+
include Tenancy::ResourceScope
|
59
|
+
|
60
|
+
scope_to :portal
|
61
|
+
validates_uniqueness_in_scope :name, case_sensitive: false
|
62
|
+
end
|
63
|
+
|
64
|
+
class Communication < ActiveRecord::Base
|
65
|
+
include Tenancy::ResourceScope
|
66
|
+
|
67
|
+
scope_to :portal, :listing
|
68
|
+
validates_uniqueness_in_scope :value
|
69
|
+
end
|
70
|
+
|
71
|
+
class ExtraCommunication < ActiveRecord::Base
|
72
|
+
include Tenancy::ResourceScope
|
73
|
+
|
74
|
+
# options here will send to #belongs_to
|
75
|
+
scope_to :portal, class_name: 'Portal'
|
76
|
+
scope_to :listing, class_name: 'Listing'
|
77
|
+
validates_uniqueness_in_scope :value
|
78
|
+
end
|
79
|
+
|
80
|
+
Portal.current = 1
|
81
|
+
Listing.find(1).to_sql
|
82
|
+
# => SELECT "listings".* FROM "listings" WHERE "portal_id" = 1 AND "id" = 1
|
83
|
+
|
84
|
+
Listing.current = 1
|
85
|
+
Communication.find(1).to_sql
|
86
|
+
# => SELECT "communications".* FROM "communications" WHERE "portal_id" = 1 AND "listing_id" = 1 AND "id" = 1
|
87
|
+
```
|
88
|
+
|
89
|
+
`scope_to :portal` does 4 things:
|
90
|
+
|
91
|
+
1. it adds `belongs_to :portal`.
|
92
|
+
|
93
|
+
2. it adds `validates :portal, presence: true`.
|
94
|
+
|
95
|
+
3. it adds `default_scope { where(portal_id: Portal.current) if Portal.current }`.
|
96
|
+
|
97
|
+
4. it overrides `#portal` so that it doesn't touch the database if `portal_id` in that record is the same as `Portal.current_id`.
|
98
|
+
|
99
|
+
`validates :value, uniqueness: true` will validates uniqueness against the whole table. `validates_uniqueness_in_scope` validates uniqueness with the scopes you passed in `scope_to`.
|
100
|
+
|
101
|
+
## Rails
|
102
|
+
|
103
|
+
Because `#current` is using thread variable, it's advisable to set to `nil` after processing controller action. This can be easily achievable by using `around_filter` and `#with` inside `application_controller.rb`. Or, you can do it manually by using `#current=`.
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
class ApplicationController < ActionController::Base
|
107
|
+
around_filter :route_domain
|
108
|
+
|
109
|
+
protected
|
110
|
+
def route_domain(&block)
|
111
|
+
Portal.with(current_portal, &block)
|
112
|
+
end
|
113
|
+
|
114
|
+
def current_portal
|
115
|
+
@current_portal ||= Portal.find_by_domain_name(request.host)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
```
|
119
|
+
|
120
|
+
## Indexes
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
add_index :listings, :portal_id
|
124
|
+
add_index :communications, [:portal_id, :listing_id]
|
125
|
+
```
|
126
|
+
|
127
|
+
## RSpec
|
128
|
+
|
129
|
+
In spec_helper.rb, you'll need to require the matchers:
|
130
|
+
|
131
|
+
```ruby
|
132
|
+
require "tenancy/matchers"
|
133
|
+
```
|
134
|
+
|
135
|
+
Example:
|
136
|
+
|
137
|
+
```ruby
|
138
|
+
describe Listing do
|
139
|
+
it { should have_scope_to(:portal) }
|
140
|
+
it { should have_scope_to(:portal).class_name('Portal') }
|
141
|
+
end
|
142
|
+
```
|
143
|
+
|
144
|
+
## Authors
|
145
|
+
|
146
|
+
* [Chamnap Chhorn](https://github.com/chamnap)
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/lib/tenancy.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'shoulda-matchers'
|
2
|
+
|
3
|
+
module Tenancy
|
4
|
+
module Shoulda
|
5
|
+
module Matchers
|
6
|
+
def have_scope_to(name)
|
7
|
+
HaveScopeToMatcher.new(name)
|
8
|
+
end
|
9
|
+
|
10
|
+
class HaveScopeToMatcher
|
11
|
+
def initialize(scope_name)
|
12
|
+
@scope_name = scope_name
|
13
|
+
@presence_matcher = ::Shoulda::Matchers::ActiveModel::ValidatePresenceOfMatcher.new(@scope_name)
|
14
|
+
@belong_to_matcher = ::Shoulda::Matchers::ActiveRecord::AssociationMatcher.new(:belongs_to, @scope_name)
|
15
|
+
end
|
16
|
+
|
17
|
+
def matches?(subject)
|
18
|
+
@presence_matches = @presence_matcher.matches?(subject)
|
19
|
+
@belong_to_matches = @belong_to_matcher.matches?(subject)
|
20
|
+
@default_scope_matches = default_scope_matches?(subject)
|
21
|
+
end
|
22
|
+
|
23
|
+
def failure_message
|
24
|
+
@presence_matcher.failure_message unless @presence_matches
|
25
|
+
@belong_to_matcher.failure_message unless @belong_to_matches
|
26
|
+
"Expected to have default_scope on :#{@scope_name}" unless @default_scope_matches
|
27
|
+
end
|
28
|
+
|
29
|
+
def description
|
30
|
+
"require to have scope_to :#{@scope_name}"
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
def default_scope_matches?(subject)
|
35
|
+
actual_class = subject.class
|
36
|
+
reflection = actual_class.reflect_on_association(@scope_name.to_sym)
|
37
|
+
scoped_class = reflection.class_name.constantize
|
38
|
+
|
39
|
+
if scoped_class.current_id
|
40
|
+
actual_class.scoped.to_sql.include? %Q{#{actual_class.quoted_table_name}.#{scoped_class.connection.quote_column_name(reflection.foreign_key)} = #{scoped_class.connection.quote(scoped_class.current_id)}}
|
41
|
+
else
|
42
|
+
true
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def method_missing(method, *args, &block)
|
47
|
+
if @belong_to_matcher.respond_to?(method)
|
48
|
+
@belong_to_matcher.send(method, *args, &block)
|
49
|
+
else
|
50
|
+
super
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
require 'rspec/core'
|
60
|
+
RSpec.configure do |config|
|
61
|
+
config.include Tenancy::Shoulda::Matchers
|
62
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Tenancy
|
2
|
+
module Resource
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
|
7
|
+
def current=(value)
|
8
|
+
tenant = case value
|
9
|
+
when self
|
10
|
+
value
|
11
|
+
when nil
|
12
|
+
nil
|
13
|
+
else
|
14
|
+
find(value)
|
15
|
+
end
|
16
|
+
|
17
|
+
Thread.current["#{name}.current"] = tenant
|
18
|
+
end
|
19
|
+
|
20
|
+
def current
|
21
|
+
Thread.current["#{name}.current"]
|
22
|
+
end
|
23
|
+
|
24
|
+
def current_id
|
25
|
+
current.try(:id)
|
26
|
+
end
|
27
|
+
|
28
|
+
def with(tenant, &block)
|
29
|
+
raise ArgumentError, "block required" if block.nil?
|
30
|
+
|
31
|
+
begin
|
32
|
+
old = self.current
|
33
|
+
self.current = tenant
|
34
|
+
|
35
|
+
block.call
|
36
|
+
ensure
|
37
|
+
self.current = old
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Tenancy
|
2
|
+
module ResourceScope
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
attr_reader :scope_fields
|
7
|
+
|
8
|
+
def scope_fields
|
9
|
+
@scope_fields ||= []
|
10
|
+
end
|
11
|
+
|
12
|
+
def scope_to(*resources)
|
13
|
+
options = resources.extract_options!.dup
|
14
|
+
raise ArgumentError, 'options should be blank if there are multiple resources' if resources.count > 1 and options.present?
|
15
|
+
|
16
|
+
resources.each do |resource|
|
17
|
+
resource = resource.to_sym
|
18
|
+
resource_class_name ||= (options[:class_name].to_s.presence || resource.to_s).classify
|
19
|
+
resource_class = resource_class_name.constantize
|
20
|
+
association_name = self.to_s.downcase.pluralize.to_sym
|
21
|
+
|
22
|
+
# validates and belongs_to
|
23
|
+
validates resource, presence: true
|
24
|
+
belongs_to resource, options
|
25
|
+
|
26
|
+
# default_scope
|
27
|
+
resource_foreign_key = reflect_on_association(resource).foreign_key
|
28
|
+
scope_fields << resource_foreign_key
|
29
|
+
default_scope { where(:"#{resource_foreign_key}" => resource_class.current_id) if resource_class.current_id }
|
30
|
+
|
31
|
+
# override to return current resource instance
|
32
|
+
# so that it doesn't touch db
|
33
|
+
define_method(resource) do |reload=false|
|
34
|
+
return super(reload) if reload
|
35
|
+
return resource_class.current if send(resource_foreign_key) == resource_class.current_id
|
36
|
+
super(reload)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def validates_uniqueness_in_scope(fields, args={})
|
42
|
+
if args[:scope]
|
43
|
+
args[:scope] = Array.wrap(args[:scope]) << scope_fields
|
44
|
+
else
|
45
|
+
args[:scope] = scope_fields
|
46
|
+
end
|
47
|
+
|
48
|
+
validates_uniqueness_of(fields, args)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "Tenancy::ResourceScope" do
|
4
|
+
let(:camyp) { Portal.create(domain_name: 'yp.com.kh') }
|
5
|
+
let(:panpages) { Portal.create(domain_name: 'panpages.com') }
|
6
|
+
let(:listing) { Listing.create(name: 'Listing 1', portal_id: camyp.id) }
|
7
|
+
|
8
|
+
after(:all) do
|
9
|
+
Portal.delete_all
|
10
|
+
end
|
11
|
+
|
12
|
+
describe Listing do
|
13
|
+
it { should belong_to(:portal) }
|
14
|
+
|
15
|
+
it { should validate_presence_of(:portal) }
|
16
|
+
|
17
|
+
it { should validate_uniqueness_of(:name).scoped_to(:portal_id).case_insensitive }
|
18
|
+
|
19
|
+
it "have default_scope with :portal_id field" do
|
20
|
+
Portal.current = camyp
|
21
|
+
|
22
|
+
Listing.scoped.to_sql.should == Listing.where(portal_id: Portal.current_id).to_sql
|
23
|
+
end
|
24
|
+
|
25
|
+
it "doesn't have default_scope when it doesn't have current portal" do
|
26
|
+
Portal.current = nil
|
27
|
+
|
28
|
+
Listing.scoped.to_sql.should == "SELECT \"listings\".* FROM \"listings\" "
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe Communication do
|
33
|
+
it { should belong_to(:portal) }
|
34
|
+
|
35
|
+
it { should validate_presence_of(:portal) }
|
36
|
+
|
37
|
+
it { should belong_to(:listing) }
|
38
|
+
|
39
|
+
it { should validate_presence_of(:listing) }
|
40
|
+
|
41
|
+
it { should validate_uniqueness_of(:value).scoped_to(:portal_id, :listing_id) }
|
42
|
+
|
43
|
+
it "have default_scope with :portal_id field" do
|
44
|
+
Portal.current = camyp
|
45
|
+
Listing.current = listing
|
46
|
+
|
47
|
+
Communication.scoped.to_sql.should == Communication.where(portal_id: Portal.current_id, listing_id: Listing.current_id).to_sql
|
48
|
+
end
|
49
|
+
|
50
|
+
it "doesn't have default_scope when it doesn't have current portal and listing" do
|
51
|
+
Portal.current = nil
|
52
|
+
Listing.current = nil
|
53
|
+
|
54
|
+
Communication.scoped.to_sql.should == "SELECT \"communications\".* FROM \"communications\" "
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe ExtraCommunication do
|
59
|
+
it { should belong_to(:portal) }
|
60
|
+
|
61
|
+
it { should belong_to(:listing) }
|
62
|
+
|
63
|
+
it "raise exception when passing two resources and options" do
|
64
|
+
expect { ExtraCommunication.scope_to(:portal, :listing, class_name: 'Listing') }.to raise_error(ArgumentError)
|
65
|
+
end
|
66
|
+
|
67
|
+
it "uses the correct scope" do
|
68
|
+
listing2 = Listing.create(name: 'Name 2', portal: camyp)
|
69
|
+
|
70
|
+
Portal.current = camyp
|
71
|
+
Listing.current = listing2
|
72
|
+
|
73
|
+
extra_communication = ExtraCommunication.new
|
74
|
+
extra_communication.listing_id.should == listing2.id
|
75
|
+
extra_communication.portal_id.should == camyp.id
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe "belongs_to method override" do
|
80
|
+
before(:each) { Portal.current = camyp }
|
81
|
+
after(:each) { Portal.current = nil }
|
82
|
+
|
83
|
+
it "reload belongs_to when passes true" do
|
84
|
+
listing.portal.domain_name = 'abc.com'
|
85
|
+
listing.portal(true).object_id.should_not == Portal.current.object_id
|
86
|
+
end
|
87
|
+
|
88
|
+
it "doesn't reload belongs_to" do
|
89
|
+
listing.portal.domain_name = 'abc.com'
|
90
|
+
listing.portal.object_id.should == Portal.current.object_id
|
91
|
+
end
|
92
|
+
|
93
|
+
it "returns different object" do
|
94
|
+
listing.portal_id = panpages.id
|
95
|
+
listing.portal.object_id.should_not == Portal.current.object_id
|
96
|
+
end
|
97
|
+
|
98
|
+
it "doesn't touch db" do
|
99
|
+
current_listing = listing
|
100
|
+
|
101
|
+
Portal.establish_connection(adapter: "sqlite3", database: "spec/invalid.sqlite3")
|
102
|
+
current_listing.portal.object_id.should == Portal.current.object_id
|
103
|
+
|
104
|
+
Portal.establish_connection(ActiveRecord::Base.connection_config)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "Tenancy::Resource" do
|
4
|
+
before(:all) do
|
5
|
+
@camyp = Portal.create(id: 1, domain_name: 'yp.com.kh')
|
6
|
+
@panpage = Portal.create(id: 2, domain_name: 'panpages.my')
|
7
|
+
@yoolk = Portal.create(id: 3, domain_name: 'yoolk.com')
|
8
|
+
end
|
9
|
+
|
10
|
+
after(:all) do
|
11
|
+
Portal.delete_all
|
12
|
+
end
|
13
|
+
|
14
|
+
before(:each) { Thread.current['Portal.current'] = nil }
|
15
|
+
|
16
|
+
it "set current with instance" do
|
17
|
+
Portal.current = @camyp
|
18
|
+
|
19
|
+
Portal.current.should == @camyp
|
20
|
+
Thread.current['Portal.current'].should == @camyp
|
21
|
+
end
|
22
|
+
|
23
|
+
it "set current with id" do
|
24
|
+
Portal.current = @panpage.id
|
25
|
+
|
26
|
+
Portal.current.should == @panpage
|
27
|
+
Thread.current['Portal.current'].should == @panpage
|
28
|
+
end
|
29
|
+
|
30
|
+
it "set current with nil" do
|
31
|
+
Portal.current = @panpage
|
32
|
+
Portal.current = nil
|
33
|
+
|
34
|
+
Portal.current.should == nil
|
35
|
+
Thread.current['Portal.current'].should == nil
|
36
|
+
end
|
37
|
+
|
38
|
+
it "#current_id" do
|
39
|
+
Portal.current = @yoolk
|
40
|
+
|
41
|
+
Portal.current_id.should == @yoolk.id
|
42
|
+
end
|
43
|
+
|
44
|
+
it "#with with block" do
|
45
|
+
Portal.current.should == nil
|
46
|
+
|
47
|
+
Portal.with(@yoolk) do
|
48
|
+
Portal.current.should == @yoolk
|
49
|
+
end
|
50
|
+
|
51
|
+
Portal.current.should == nil
|
52
|
+
end
|
53
|
+
|
54
|
+
it "#with without block" do
|
55
|
+
expect { Portal.with(@yoolk) }.to raise_error(ArgumentError)
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'tenancy/matchers'
|
3
|
+
|
4
|
+
describe ExtraCommunication do
|
5
|
+
let(:camyp) { Portal.create(domain_name: 'yp.com.kh') }
|
6
|
+
before { Portal.current = camyp }
|
7
|
+
|
8
|
+
it { should have_scope_to(:portal) }
|
9
|
+
it { should have_scope_to(:portal).class_name('Portal') }
|
10
|
+
it { should have_scope_to(:listing) }
|
11
|
+
it { should have_scope_to(:listing).class_name('Listing') }
|
12
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'tenancy'
|
2
|
+
|
3
|
+
# active_record
|
4
|
+
load File.dirname(__FILE__) + '/support/schema.rb'
|
5
|
+
load File.dirname(__FILE__) + '/support/models.rb'
|
6
|
+
|
7
|
+
require 'pry'
|
8
|
+
require 'shoulda-matchers'
|
9
|
+
|
10
|
+
RSpec.configure do |config|
|
11
|
+
config.filter_run focus: true
|
12
|
+
config.run_all_when_everything_filtered = true
|
13
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
14
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class Portal < ActiveRecord::Base
|
2
|
+
include Tenancy::Resource
|
3
|
+
end
|
4
|
+
|
5
|
+
class Listing < ActiveRecord::Base
|
6
|
+
include Tenancy::Resource
|
7
|
+
include Tenancy::ResourceScope
|
8
|
+
|
9
|
+
scope_to :portal
|
10
|
+
validates_uniqueness_in_scope :name, case_sensitive: false
|
11
|
+
end
|
12
|
+
|
13
|
+
class Communication < ActiveRecord::Base
|
14
|
+
include Tenancy::ResourceScope
|
15
|
+
|
16
|
+
scope_to :portal, :listing
|
17
|
+
validates_uniqueness_in_scope :value
|
18
|
+
end
|
19
|
+
|
20
|
+
class ExtraCommunication < ActiveRecord::Base
|
21
|
+
include Tenancy::ResourceScope
|
22
|
+
|
23
|
+
scope_to :portal, class_name: 'Portal'
|
24
|
+
scope_to :listing, class_name: 'Listing'
|
25
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: 'spec/test.sqlite3')
|
5
|
+
ActiveRecord::Migration.verbose = false
|
6
|
+
|
7
|
+
ActiveRecord::Schema.define do
|
8
|
+
self.verbose = false
|
9
|
+
|
10
|
+
create_table :portals, :force => true do |t|
|
11
|
+
t.string :domain_name
|
12
|
+
t.timestamps
|
13
|
+
end
|
14
|
+
|
15
|
+
create_table :listings, :force => true do |t|
|
16
|
+
t.string :name
|
17
|
+
t.references :portal
|
18
|
+
t.timestamps
|
19
|
+
end
|
20
|
+
|
21
|
+
create_table :communications, :force => true do |t|
|
22
|
+
t.string :label
|
23
|
+
t.string :value
|
24
|
+
t.references :listing
|
25
|
+
t.references :portal
|
26
|
+
t.timestamps
|
27
|
+
end
|
28
|
+
|
29
|
+
create_table :extra_communications, :force => true do |t|
|
30
|
+
t.string :label
|
31
|
+
t.string :value
|
32
|
+
t.references :listing
|
33
|
+
t.references :portal
|
34
|
+
t.timestamps
|
35
|
+
end
|
36
|
+
end
|
data/tenancy.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'tenancy/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "tenancy"
|
8
|
+
spec.version = Tenancy::VERSION
|
9
|
+
spec.authors = ["chamnap"]
|
10
|
+
spec.email = ["chamnapchhorn@gmail.com"]
|
11
|
+
spec.description = %q{A simple multi-tenancy with activerecord through scoping}
|
12
|
+
spec.summary = %q{A simple multi-tenancy with activerecord through scoping}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "rspec", "~> 2.12.0"
|
22
|
+
spec.add_development_dependency "shoulda", "~> 3.5.0"
|
23
|
+
spec.add_development_dependency "pry", "~> 0.9.12"
|
24
|
+
spec.add_development_dependency "sqlite3", "~> 1.3.7"
|
25
|
+
spec.add_development_dependency "rake"
|
26
|
+
|
27
|
+
spec.add_dependency "activerecord", "~> 3.2.13"
|
28
|
+
end
|
metadata
ADDED
@@ -0,0 +1,153 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tenancy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- chamnap
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-11-02 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - ~>
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: 2.12.0
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ~>
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 2.12.0
|
24
|
+
type: :development
|
25
|
+
prerelease: false
|
26
|
+
name: rspec
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ~>
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 3.5.0
|
33
|
+
version_requirements: !ruby/object:Gem::Requirement
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 3.5.0
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
name: shoulda
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ~>
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 0.9.12
|
47
|
+
version_requirements: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ~>
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: 0.9.12
|
52
|
+
type: :development
|
53
|
+
prerelease: false
|
54
|
+
name: pry
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ~>
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: 1.3.7
|
61
|
+
version_requirements: !ruby/object:Gem::Requirement
|
62
|
+
requirements:
|
63
|
+
- - ~>
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: 1.3.7
|
66
|
+
type: :development
|
67
|
+
prerelease: false
|
68
|
+
name: sqlite3
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ! '>='
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
version_requirements: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - ! '>='
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '0'
|
80
|
+
type: :development
|
81
|
+
prerelease: false
|
82
|
+
name: rake
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ~>
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: 3.2.13
|
89
|
+
version_requirements: !ruby/object:Gem::Requirement
|
90
|
+
requirements:
|
91
|
+
- - ~>
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: 3.2.13
|
94
|
+
type: :runtime
|
95
|
+
prerelease: false
|
96
|
+
name: activerecord
|
97
|
+
description: A simple multi-tenancy with activerecord through scoping
|
98
|
+
email:
|
99
|
+
- chamnapchhorn@gmail.com
|
100
|
+
executables: []
|
101
|
+
extensions: []
|
102
|
+
extra_rdoc_files: []
|
103
|
+
files:
|
104
|
+
- .gitignore
|
105
|
+
- .rspec
|
106
|
+
- .rvmrc
|
107
|
+
- Gemfile
|
108
|
+
- LICENSE.txt
|
109
|
+
- README.md
|
110
|
+
- Rakefile
|
111
|
+
- lib/tenancy.rb
|
112
|
+
- lib/tenancy/matchers.rb
|
113
|
+
- lib/tenancy/resource.rb
|
114
|
+
- lib/tenancy/resource_scope.rb
|
115
|
+
- lib/tenancy/version.rb
|
116
|
+
- spec/lib/resource_scope_spec.rb
|
117
|
+
- spec/lib/resource_spec.rb
|
118
|
+
- spec/lib/shoulda_matchers_spec.rb
|
119
|
+
- spec/spec_helper.rb
|
120
|
+
- spec/support/models.rb
|
121
|
+
- spec/support/schema.rb
|
122
|
+
- tenancy.gemspec
|
123
|
+
homepage: ''
|
124
|
+
licenses:
|
125
|
+
- MIT
|
126
|
+
metadata: {}
|
127
|
+
post_install_message:
|
128
|
+
rdoc_options: []
|
129
|
+
require_paths:
|
130
|
+
- lib
|
131
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
132
|
+
requirements:
|
133
|
+
- - ! '>='
|
134
|
+
- !ruby/object:Gem::Version
|
135
|
+
version: '0'
|
136
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
137
|
+
requirements:
|
138
|
+
- - ! '>='
|
139
|
+
- !ruby/object:Gem::Version
|
140
|
+
version: '0'
|
141
|
+
requirements: []
|
142
|
+
rubyforge_project:
|
143
|
+
rubygems_version: 2.0.7
|
144
|
+
signing_key:
|
145
|
+
specification_version: 4
|
146
|
+
summary: A simple multi-tenancy with activerecord through scoping
|
147
|
+
test_files:
|
148
|
+
- spec/lib/resource_scope_spec.rb
|
149
|
+
- spec/lib/resource_spec.rb
|
150
|
+
- spec/lib/shoulda_matchers_spec.rb
|
151
|
+
- spec/spec_helper.rb
|
152
|
+
- spec/support/models.rb
|
153
|
+
- spec/support/schema.rb
|