account_scopper 0.1.0 → 0.2.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.
- 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
|