dynamic_default_scoping 0.0.1
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/.gitignore +5 -0
- data/.rspec +3 -0
- data/Gemfile +4 -0
- data/README.rdoc +53 -0
- data/Rakefile +12 -0
- data/dynamic_default_scoping.gemspec +26 -0
- data/lib/dynamic_default_scoping.rb +69 -0
- data/lib/dynamic_default_scoping/version.rb +3 -0
- data/spec/spec_helper.rb +43 -0
- data/spec/unit/dynamic_default_scoping_spec.rb +114 -0
- metadata +125 -0
data/.rspec
ADDED
data/Gemfile
ADDED
data/README.rdoc
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
= Dynamic Default Scoping
|
2
|
+
|
3
|
+
== What is the library for?
|
4
|
+
|
5
|
+
Well, at the moment ActiveRecord (3.0.3) does not allow to pass
|
6
|
+
procs and lambdas to +default_scope+. That's strange, isn't it?
|
7
|
+
Because +scope+ successfully handles arguments of those types,
|
8
|
+
so why +default_scope+ does not?
|
9
|
+
|
10
|
+
== How to use?
|
11
|
+
|
12
|
+
Here is an example:
|
13
|
+
|
14
|
+
class Post < ActiveRecord::Base
|
15
|
+
include DynamicDefaultScoping
|
16
|
+
|
17
|
+
default_scope :language, lambda { |*args|
|
18
|
+
case locale = (args[0].presence || I18n.locale).to_s
|
19
|
+
when 'any', 'all'
|
20
|
+
where :locale => I18n.available_locales.map(&:to_s)
|
21
|
+
else
|
22
|
+
where :locale => locale
|
23
|
+
end
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
You can overwrite the default value for the locale attribute
|
28
|
+
calling the scope by name and passing another value.
|
29
|
+
|
30
|
+
== What are the constrains?
|
31
|
+
|
32
|
+
Because of the internal implementation of ActiveRecord and friends,
|
33
|
+
the library *should be included only after* all calls to
|
34
|
+
the regular +default_scope+ and +scope+, especially when they use
|
35
|
+
new aRel syntax like +where+, +order+, +limit+, etc.
|
36
|
+
|
37
|
+
Here is how it should be:
|
38
|
+
|
39
|
+
class Article < ActiveRecord::Base
|
40
|
+
default_scope order(:created_at).reverse_order
|
41
|
+
|
42
|
+
scope :published, where(:published => true)
|
43
|
+
|
44
|
+
include DynamicDefaultScoping
|
45
|
+
|
46
|
+
default_scope :recent, lambda {
|
47
|
+
where [ 'created_at > ?', Time.now - 5.days ]
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
== When there will be a normal implementation of this feature?
|
52
|
+
|
53
|
+
http://rails.lighthouseapp.com/projects/8994/tickets/1812-default_scope-cant-take-procs
|
data/Rakefile
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "dynamic_default_scoping/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "dynamic_default_scoping"
|
7
|
+
s.version = DynamicDefaultScoping::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Ivan Ukhov"]
|
10
|
+
s.email = ["uvsoft@gmail.com"]
|
11
|
+
s.homepage = ""
|
12
|
+
s.summary = %q{Make ActiveRecord::Base.default_scope take a proc and have a name.}
|
13
|
+
s.description = %q{Being included in a model, the library patches ActiveRecord::Base.default_scope to take a proc and have an optional name.}
|
14
|
+
|
15
|
+
s.rubyforge_project = "dynamic_default_scoping"
|
16
|
+
|
17
|
+
s.add_development_dependency 'mysql2', '~> 0.2.6'
|
18
|
+
s.add_development_dependency 'rspec', '~> 2.4.0'
|
19
|
+
|
20
|
+
s.add_dependency 'rails', '3.0.3'
|
21
|
+
|
22
|
+
s.files = `git ls-files`.split("\n")
|
23
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
24
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
25
|
+
s.require_paths = ["lib"]
|
26
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module DynamicDefaultScoping
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
@relation = nil
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def default_scope name, options = nil
|
10
|
+
if options.present?
|
11
|
+
scope name, options
|
12
|
+
else
|
13
|
+
name, options = nil, name
|
14
|
+
end
|
15
|
+
|
16
|
+
reset_scoped_methods
|
17
|
+
|
18
|
+
scoping = self.default_scoping.dup
|
19
|
+
last = scoping.pop
|
20
|
+
|
21
|
+
scope = if last.respond_to?(:call) || options.respond_to?(:call)
|
22
|
+
lambda do
|
23
|
+
construct_finder_arel \
|
24
|
+
options.respond_to?(:call) ? options.call : options,
|
25
|
+
last.respond_to?(:call) ? last.call : last
|
26
|
+
end
|
27
|
+
else
|
28
|
+
construct_finder_arel options, last
|
29
|
+
end
|
30
|
+
|
31
|
+
self.default_scoping = scoping << scope
|
32
|
+
end
|
33
|
+
|
34
|
+
def scoped options = nil
|
35
|
+
if options
|
36
|
+
scoped.apply_finder_options options
|
37
|
+
else
|
38
|
+
last = current_scoped_methods
|
39
|
+
last ? relation.merge(last) : relation.clone
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
protected
|
44
|
+
|
45
|
+
def current_scoped_methods
|
46
|
+
method = scoped_methods.last
|
47
|
+
method.kind_of?(Proc) ? unscoped(&method) : method
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def relation
|
53
|
+
@relation ||= DynamicDefaultScoping::Relation.new self, arel_table
|
54
|
+
finder_needs_type_condition? ? @relation.where(type_condition) : @relation
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class Relation < ::ActiveRecord::Relation
|
59
|
+
protected
|
60
|
+
|
61
|
+
def method_missing method, *args, &block
|
62
|
+
if @klass.scopes[method].present?
|
63
|
+
merge @klass.unscoped { @klass.send method, *args, &block }
|
64
|
+
else
|
65
|
+
super
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
2
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
|
3
|
+
|
4
|
+
require 'active_support/all'
|
5
|
+
require 'active_record'
|
6
|
+
require 'dynamic_default_scoping'
|
7
|
+
|
8
|
+
RSpec.configure do |config|
|
9
|
+
config.before(:each) do
|
10
|
+
Post.unscoped.delete_all
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
ActiveRecord::Base.establish_connection \
|
15
|
+
:adapter => 'mysql2',
|
16
|
+
:hostname => 'localhost',
|
17
|
+
:database => 'dynamic_default_scoping',
|
18
|
+
:username => 'root',
|
19
|
+
:password => ''
|
20
|
+
|
21
|
+
connection = ActiveRecord::Base.connection
|
22
|
+
connection.drop_table 'posts' if connection.table_exists? 'posts'
|
23
|
+
connection.create_table 'posts' do |table|
|
24
|
+
table.string :locale
|
25
|
+
table.timestamps
|
26
|
+
end
|
27
|
+
|
28
|
+
LOCALES = [ :en, :ru ]
|
29
|
+
|
30
|
+
class Post < ActiveRecord::Base
|
31
|
+
scope :recent, limit(5)
|
32
|
+
|
33
|
+
include DynamicDefaultScoping
|
34
|
+
|
35
|
+
default_scope :language, lambda { |*args|
|
36
|
+
case locale = (args[0].presence || I18n.locale).to_s
|
37
|
+
when 'any', 'all'
|
38
|
+
where :locale => LOCALES.map(&:to_s)
|
39
|
+
else
|
40
|
+
where :locale => locale
|
41
|
+
end
|
42
|
+
}
|
43
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe DynamicDefaultScoping do
|
4
|
+
def should_have_valid_locale *args
|
5
|
+
options = args.extract_options!
|
6
|
+
locale = (options[:locale].presence || I18n.locale).to_s
|
7
|
+
args.flatten.all?{ |post| post.locale == locale }.should be_true
|
8
|
+
end
|
9
|
+
|
10
|
+
let(:locales) { LOCALES }
|
11
|
+
let(:per_locale) do
|
12
|
+
hash = {}
|
13
|
+
locales.each_with_index { |locale, i| hash[locale] = 3 + i }
|
14
|
+
hash
|
15
|
+
end
|
16
|
+
|
17
|
+
describe 'when switching between locales' do
|
18
|
+
before do
|
19
|
+
locales.each do |locale|
|
20
|
+
per_locale[locale].times do
|
21
|
+
Post.create! :locale => locale.to_s
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
around do |block|
|
27
|
+
locales.each do |locale|
|
28
|
+
I18n.locale = locale
|
29
|
+
block.call
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'when the locale is not specified' do
|
34
|
+
it 'finds records only in the current locale' do
|
35
|
+
should_have_valid_locale Post.first, Post.all
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'counts records only in the current locale' do
|
39
|
+
Post.count.should == per_locale[I18n.locale]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context 'when the locale is specified' do
|
44
|
+
shared_examples_for 'well localized class' do
|
45
|
+
it 'generates correct query' do
|
46
|
+
sql = @scoped.to_sql
|
47
|
+
sql.should include %[`locale` = '#{ @locale }']
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'finds records in the specified locale' do
|
51
|
+
all = @scoped.all
|
52
|
+
all.length.should == per_locale[@locale]
|
53
|
+
should_have_valid_locale all, :locale => @locale
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'counts records in the specified locale' do
|
57
|
+
@scoped.count.should == per_locale[@locale]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
context 'when there is another scope before' do
|
62
|
+
around do |block|
|
63
|
+
locales.each do |locale|
|
64
|
+
@locale = locale
|
65
|
+
@scoped = Post.recent.language(locale)
|
66
|
+
block.call
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
it_behaves_like 'well localized class'
|
71
|
+
end
|
72
|
+
|
73
|
+
context 'when there is another scope after' do
|
74
|
+
around do |block|
|
75
|
+
locales.each do |locale|
|
76
|
+
@locale = locale
|
77
|
+
@scoped = Post.language(locale).recent
|
78
|
+
block.call
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
it_behaves_like 'well localized class'
|
83
|
+
end
|
84
|
+
|
85
|
+
context 'when there is no another scope' do
|
86
|
+
around do |block|
|
87
|
+
locales.each do |locale|
|
88
|
+
@locale = locale
|
89
|
+
@scoped = Post.language(locale)
|
90
|
+
block.call
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
it_behaves_like 'well localized class'
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
context 'when we are interested in all locales' do
|
99
|
+
let(:scoped) { Post.language :any }
|
100
|
+
|
101
|
+
it 'finds all records' do
|
102
|
+
sql = scoped.to_sql
|
103
|
+
locales.each do |locale|
|
104
|
+
sql.should =~ %r[`locale` IN \(.*'#{ locale }'.*\)]
|
105
|
+
end
|
106
|
+
scoped.length.should == per_locale.values.sum
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'counts all records' do
|
110
|
+
scoped.count.should == per_locale.values.sum
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
metadata
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dynamic_default_scoping
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Ivan Ukhov
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-01-30 00:00:00 +03:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ~>
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 27
|
29
|
+
segments:
|
30
|
+
- 0
|
31
|
+
- 2
|
32
|
+
- 6
|
33
|
+
version: 0.2.6
|
34
|
+
type: :development
|
35
|
+
requirement: *id001
|
36
|
+
name: mysql2
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
prerelease: false
|
39
|
+
version_requirements: &id002 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ~>
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
hash: 31
|
45
|
+
segments:
|
46
|
+
- 2
|
47
|
+
- 4
|
48
|
+
- 0
|
49
|
+
version: 2.4.0
|
50
|
+
type: :development
|
51
|
+
requirement: *id002
|
52
|
+
name: rspec
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
prerelease: false
|
55
|
+
version_requirements: &id003 !ruby/object:Gem::Requirement
|
56
|
+
none: false
|
57
|
+
requirements:
|
58
|
+
- - "="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
hash: 1
|
61
|
+
segments:
|
62
|
+
- 3
|
63
|
+
- 0
|
64
|
+
- 3
|
65
|
+
version: 3.0.3
|
66
|
+
type: :runtime
|
67
|
+
requirement: *id003
|
68
|
+
name: rails
|
69
|
+
description: Being included in a model, the library patches ActiveRecord::Base.default_scope to take a proc and have an optional name.
|
70
|
+
email:
|
71
|
+
- uvsoft@gmail.com
|
72
|
+
executables: []
|
73
|
+
|
74
|
+
extensions: []
|
75
|
+
|
76
|
+
extra_rdoc_files: []
|
77
|
+
|
78
|
+
files:
|
79
|
+
- .gitignore
|
80
|
+
- .rspec
|
81
|
+
- Gemfile
|
82
|
+
- README.rdoc
|
83
|
+
- Rakefile
|
84
|
+
- dynamic_default_scoping.gemspec
|
85
|
+
- lib/dynamic_default_scoping.rb
|
86
|
+
- lib/dynamic_default_scoping/version.rb
|
87
|
+
- spec/spec_helper.rb
|
88
|
+
- spec/unit/dynamic_default_scoping_spec.rb
|
89
|
+
has_rdoc: true
|
90
|
+
homepage: ""
|
91
|
+
licenses: []
|
92
|
+
|
93
|
+
post_install_message:
|
94
|
+
rdoc_options: []
|
95
|
+
|
96
|
+
require_paths:
|
97
|
+
- lib
|
98
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
99
|
+
none: false
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
hash: 3
|
104
|
+
segments:
|
105
|
+
- 0
|
106
|
+
version: "0"
|
107
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
108
|
+
none: false
|
109
|
+
requirements:
|
110
|
+
- - ">="
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
hash: 3
|
113
|
+
segments:
|
114
|
+
- 0
|
115
|
+
version: "0"
|
116
|
+
requirements: []
|
117
|
+
|
118
|
+
rubyforge_project: dynamic_default_scoping
|
119
|
+
rubygems_version: 1.4.2
|
120
|
+
signing_key:
|
121
|
+
specification_version: 3
|
122
|
+
summary: Make ActiveRecord::Base.default_scope take a proc and have a name.
|
123
|
+
test_files:
|
124
|
+
- spec/spec_helper.rb
|
125
|
+
- spec/unit/dynamic_default_scoping_spec.rb
|