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.
@@ -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. Make relationship between Account and other models
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
- cattr_accessor :current_account
36
+ class Account < ActiveRecord::Base
37
+ cattr_accessor :current_account
23
38
 
24
- has_many :users
25
- end
39
+ has_many :users
40
+ end
26
41
 
27
42
  # app/model/user.rb
28
43
 
29
- class User < ActiveRecord::Base
30
- belongs_to :account
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
- before_filter :set_current_account
37
- # ...
50
+ class ApplicationController < ActionController::Base
51
+ before_filter :set_current_account
52
+ # ...
38
53
 
39
- private
40
- # Don't forget that @current_account should be properly setup to be an Account object
41
- # for this purpose you can use the plugin AccountLocation from David Heinemeier Hansson
42
- def set_current_account
43
- Account.current_account = @current_account
44
- end
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
1
+ 0.2.0
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{account_scopper}
8
- s.version = "0.1.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-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
@@ -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
@@ -1,3 +1,6 @@
1
1
  class User < ActiveRecord::Base
2
2
  belongs_to :account
3
+
4
+ validates_uniqueness_of :name
5
+ validates_global_uniqueness_of :login
3
6
  end
@@ -2,11 +2,14 @@ seb:
2
2
  id: 1
3
3
  account_id: 1
4
4
  name: Seb
5
+ login: seb
5
6
  marc:
6
7
  id: 2
7
8
  account_id: 1
8
9
  name: Marc
10
+ login: marc
9
11
  stephane:
10
12
  id: 3
11
13
  account_id: 2
12
14
  name: Stephane
15
+ login: stephane
@@ -4,6 +4,7 @@ ActiveRecord::Schema.define(:version => 0) do
4
4
  end
5
5
  create_table :users, :force => true do |t|
6
6
  t.string :name
7
+ t.string :login
7
8
  t.references :account
8
9
  end
9
10
  end
@@ -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.1.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-11 00:00:00 +02:00
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