dynamic_default_scoping 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ pkg/*
2
+ *.gem
3
+ .bundle
4
+ *.swp
5
+ Gemfile.lock
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --colour
2
+ --format documentation
3
+ --fail-fast
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in dynamic_default_scoping.gemspec
4
+ gemspec
@@ -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
@@ -0,0 +1,12 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+ Bundler.setup
4
+
5
+ require 'rspec'
6
+ require 'rspec/core/rake_task'
7
+
8
+ Rspec::Core::RakeTask.new(:spec) do |spec|
9
+ spec.pattern = "spec/**/*_spec.rb"
10
+ end
11
+
12
+ task :default => :spec
@@ -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
@@ -0,0 +1,3 @@
1
+ module DynamicDefaultScoping
2
+ VERSION = "0.0.1"
3
+ end
@@ -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