account_scopper 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +43 -18
- data/Rakefile +1 -0
- data/VERSION +1 -1
- data/account_scopper.gemspec +5 -2
- data/lib/account_scopper.rb +74 -0
- data/spec/account_scopper_spec.rb +15 -0
- data/spec/fixtures/user.rb +3 -0
- data/spec/fixtures/users.yml +3 -0
- data/spec/schema.rb +1 -0
- data/spec/spec_helper.rb +2 -0
- metadata +12 -2
data/README.rdoc
CHANGED
@@ -4,13 +4,28 @@ Account Scopper aims to make conversion from a single account to multi-accounts
|
|
4
4
|
|
5
5
|
Simply set your current_account before each request and the scoping will be done on it's own.
|
6
6
|
|
7
|
+
== Install
|
8
|
+
|
9
|
+
If new to gemcutter first run
|
10
|
+
|
11
|
+
sudo gem install gemcutter
|
12
|
+
sudo gemcutter tumble
|
13
|
+
|
14
|
+
Then run
|
15
|
+
|
16
|
+
sudo gem install account_scopper
|
17
|
+
|
18
|
+
If using rails, in your `config/environment.rb`
|
19
|
+
|
20
|
+
config.gem 'account_scopper'
|
21
|
+
|
7
22
|
== Usage
|
8
23
|
|
9
24
|
To convert your application, you will need to make few changes :
|
10
25
|
|
11
26
|
1. Create an Account model and Accounts table
|
12
27
|
2. Add account_id to your existing models (except Account of course)
|
13
|
-
3.
|
28
|
+
3. Define relationship between Account and other models
|
14
29
|
4. Add the class variable "current_account" to the model Account
|
15
30
|
5. Initialize Account.current_account before every request (eg: app/controllers/application.rb)
|
16
31
|
|
@@ -18,31 +33,31 @@ eg:
|
|
18
33
|
|
19
34
|
# app/model/account.rb
|
20
35
|
|
21
|
-
class Account < ActiveRecord::Base
|
22
|
-
|
36
|
+
class Account < ActiveRecord::Base
|
37
|
+
cattr_accessor :current_account
|
23
38
|
|
24
|
-
|
25
|
-
end
|
39
|
+
has_many :users
|
40
|
+
end
|
26
41
|
|
27
42
|
# app/model/user.rb
|
28
43
|
|
29
|
-
class User < ActiveRecord::Base
|
30
|
-
|
31
|
-
end
|
44
|
+
class User < ActiveRecord::Base
|
45
|
+
belongs_to :account
|
46
|
+
end
|
32
47
|
|
33
48
|
# app/controllers/application.rb
|
34
49
|
|
35
|
-
class ApplicationController < ActionController::Base
|
36
|
-
|
37
|
-
|
50
|
+
class ApplicationController < ActionController::Base
|
51
|
+
before_filter :set_current_account
|
52
|
+
# ...
|
38
53
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
end
|
54
|
+
private
|
55
|
+
# Don't forget that @current_account should be properly setup to be an Account object
|
56
|
+
# for this purpose you can use the plugin AccountLocation from David Heinemeier Hansson
|
57
|
+
def set_current_account
|
58
|
+
Account.current_account = @current_account
|
59
|
+
end
|
60
|
+
end
|
46
61
|
|
47
62
|
== What it does
|
48
63
|
|
@@ -51,6 +66,16 @@ The plugin overwrite few methods from ActiveRecord::Base
|
|
51
66
|
To have a better understanding of what it does, the best way is still to look
|
52
67
|
at the code itself and the tests.
|
53
68
|
|
69
|
+
== Helpers
|
70
|
+
|
71
|
+
As account_scopper scopes all database requests, `validates_uniqueness_of` will also scope to the current account.
|
72
|
+
|
73
|
+
In some cases, you may like to make sure that somethings remain unique to your whole system.
|
74
|
+
|
75
|
+
(eg: the user login or email if logging in from a common page for all accounts)
|
76
|
+
|
77
|
+
For this purpose, you can use `validates_global_uniqueness_of`. This validations will make sure your attribute is unique over all accounts.
|
78
|
+
|
54
79
|
== Warning
|
55
80
|
|
56
81
|
If using manually generated database requests like find_by_sql, ... you'll have to do your own scoping
|
data/Rakefile
CHANGED
@@ -10,6 +10,7 @@ begin
|
|
10
10
|
gem.email = "public@zencocoon.com"
|
11
11
|
gem.homepage = "http://github.com/ZenCocoon/account_scopper"
|
12
12
|
gem.authors = ["Sebastien Grosjean"]
|
13
|
+
gem.add_dependency "activerecord", ">= 2.3.4"
|
13
14
|
gem.add_development_dependency "rspec", ">= 1.2.9"
|
14
15
|
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
15
16
|
end
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.2.0
|
data/account_scopper.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{account_scopper}
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.2.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Sebastien Grosjean"]
|
12
|
-
s.date = %q{2009-11-
|
12
|
+
s.date = %q{2009-11-17}
|
13
13
|
s.description = %q{Account Scopper: Automatically scope your ActiveRecord's model by account. Ideal for multi-account applications.}
|
14
14
|
s.email = %q{public@zencocoon.com}
|
15
15
|
s.extra_rdoc_files = [
|
@@ -62,11 +62,14 @@ Gem::Specification.new do |s|
|
|
62
62
|
s.specification_version = 3
|
63
63
|
|
64
64
|
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
65
|
+
s.add_runtime_dependency(%q<activerecord>, [">= 2.3.4"])
|
65
66
|
s.add_development_dependency(%q<rspec>, [">= 1.2.9"])
|
66
67
|
else
|
68
|
+
s.add_dependency(%q<activerecord>, [">= 2.3.4"])
|
67
69
|
s.add_dependency(%q<rspec>, [">= 1.2.9"])
|
68
70
|
end
|
69
71
|
else
|
72
|
+
s.add_dependency(%q<activerecord>, [">= 2.3.4"])
|
70
73
|
s.add_dependency(%q<rspec>, [">= 1.2.9"])
|
71
74
|
end
|
72
75
|
end
|
data/lib/account_scopper.rb
CHANGED
@@ -55,4 +55,78 @@ module ActiveRecord # :nodoc:
|
|
55
55
|
orig_create
|
56
56
|
end
|
57
57
|
end
|
58
|
+
|
59
|
+
module Validations
|
60
|
+
module ClassMethods
|
61
|
+
# Allow to validates uniqueness without scoping to the current account.
|
62
|
+
def validates_global_uniqueness_of(*attr_names)
|
63
|
+
configuration = { :case_sensitive => true }
|
64
|
+
configuration.update(attr_names.extract_options!)
|
65
|
+
|
66
|
+
validates_each(attr_names,configuration) do |record, attr_name, value|
|
67
|
+
|
68
|
+
# The check for an existing value should be run from a class that
|
69
|
+
# isn't abstract. This means working down from the current class
|
70
|
+
# (self), to the first non-abstract class. Since classes don't know
|
71
|
+
# their subclasses, we have to build the hierarchy between self and
|
72
|
+
# the record's class.
|
73
|
+
class_hierarchy = [record.class]
|
74
|
+
while class_hierarchy.first != self
|
75
|
+
class_hierarchy.insert(0, class_hierarchy.first.superclass)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Now we can work our way down the tree to the first non-abstract
|
79
|
+
# class (which has a database table to query from).
|
80
|
+
finder_class = class_hierarchy.detect { |klass| !klass.abstract_class? }
|
81
|
+
|
82
|
+
column = finder_class.columns_hash[attr_name.to_s]
|
83
|
+
|
84
|
+
if value.nil?
|
85
|
+
comparison_operator = "IS ?"
|
86
|
+
elsif column.text?
|
87
|
+
comparison_operator = "#{connection.case_sensitive_equality_operator} ?"
|
88
|
+
value = column.limit ? value.to_s.mb_chars[0, column.limit] : value.to_s
|
89
|
+
else
|
90
|
+
comparison_operator = "= ?"
|
91
|
+
end
|
92
|
+
|
93
|
+
sql_attribute = "#{record.class.quoted_table_name}.#{connection.quote_column_name(attr_name)}"
|
94
|
+
|
95
|
+
if value.nil? || (configuration[:case_sensitive] || !column.text?)
|
96
|
+
condition_sql = "#{sql_attribute} #{comparison_operator}"
|
97
|
+
condition_params = [value]
|
98
|
+
else
|
99
|
+
condition_sql = "LOWER(#{sql_attribute}) #{comparison_operator}"
|
100
|
+
condition_params = [value.mb_chars.downcase]
|
101
|
+
end
|
102
|
+
|
103
|
+
if scope = configuration[:scope]
|
104
|
+
Array(scope).map do |scope_item|
|
105
|
+
scope_value = record.send(scope_item)
|
106
|
+
condition_sql << " AND " << attribute_condition("#{record.class.quoted_table_name}.#{scope_item}", scope_value)
|
107
|
+
condition_params << scope_value
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
unless record.new_record?
|
112
|
+
condition_sql << " AND #{record.class.quoted_table_name}.#{record.class.primary_key} <> ?"
|
113
|
+
condition_params << record.send(:id)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Remove current account to prevent scopping
|
117
|
+
account = Account.current_account
|
118
|
+
Account.current_account = nil
|
119
|
+
|
120
|
+
finder_class.with_exclusive_scope do
|
121
|
+
if finder_class.exists?([condition_sql, *condition_params])
|
122
|
+
record.errors.add(attr_name, :taken, :default => configuration[:message], :value => value)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Restore current account
|
127
|
+
Account.current_account = account
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
58
132
|
end
|
@@ -66,4 +66,19 @@ describe "AccountScopper" do
|
|
66
66
|
user = User.find_by_name('Stephane')
|
67
67
|
user.destroy.should_not be_nil
|
68
68
|
end
|
69
|
+
|
70
|
+
it "should validates_uniqueness_of within account" do
|
71
|
+
u = User.create(:name => "Seb")
|
72
|
+
u.errors.on(:name).should == "has already been taken"
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should not validates_uniqueness_of without scoping the current account" do
|
76
|
+
u = User.create(:name => "Stephane")
|
77
|
+
u.valid?.should be_true
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should validates_global_uniqueness_of wihtout scoping the current account" do
|
81
|
+
u = User.create(:login => "stephane")
|
82
|
+
u.errors.on(:login).should == "has already been taken"
|
83
|
+
end
|
69
84
|
end
|
data/spec/fixtures/user.rb
CHANGED
data/spec/fixtures/users.yml
CHANGED
data/spec/schema.rb
CHANGED
data/spec/spec_helper.rb
CHANGED
@@ -9,6 +9,8 @@ require File.dirname(__FILE__) + '/lib/load_schema'
|
|
9
9
|
require File.dirname(__FILE__) + '/lib/load_models'
|
10
10
|
require File.dirname(__FILE__) + '/lib/load_fixtures'
|
11
11
|
|
12
|
+
require File.dirname(__FILE__) + "/../init"
|
13
|
+
|
12
14
|
Spec::Runner.configure do |config|
|
13
15
|
load_models
|
14
16
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: account_scopper
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sebastien Grosjean
|
@@ -9,9 +9,19 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-11-
|
12
|
+
date: 2009-11-17 00:00:00 +02:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: activerecord
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 2.3.4
|
24
|
+
version:
|
15
25
|
- !ruby/object:Gem::Dependency
|
16
26
|
name: rspec
|
17
27
|
type: :development
|