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.
@@ -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