tenancy 0.1.0
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.
- 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
|