counter_culture 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.rspec +1 -0
- data/.rvmrc +1 -0
- data/Gemfile +22 -0
- data/Gemfile.lock +109 -0
- data/LICENSE.txt +20 -0
- data/README.md +22 -0
- data/Rakefile +50 -0
- data/VERSION +1 -0
- data/counter_culture.gemspec +124 -0
- data/lib/counter_culture.rb +260 -0
- data/spec/counter_culture_spec.rb +232 -0
- data/spec/models/company.rb +3 -0
- data/spec/models/industry.rb +2 -0
- data/spec/models/product.rb +2 -0
- data/spec/models/review.rb +14 -0
- data/spec/models/user.rb +3 -0
- data/spec/rails_app/.gitignore +15 -0
- data/spec/rails_app/Gemfile +38 -0
- data/spec/rails_app/README.rdoc +261 -0
- data/spec/rails_app/Rakefile +7 -0
- data/spec/rails_app/app/assets/images/rails.png +0 -0
- data/spec/rails_app/app/assets/javascripts/application.js +15 -0
- data/spec/rails_app/app/assets/stylesheets/application.css +13 -0
- data/spec/rails_app/app/controllers/application_controller.rb +3 -0
- data/spec/rails_app/app/helpers/application_helper.rb +2 -0
- data/spec/rails_app/app/mailers/.gitkeep +0 -0
- data/spec/rails_app/app/models/.gitkeep +0 -0
- data/spec/rails_app/app/views/layouts/application.html.erb +14 -0
- data/spec/rails_app/config/application.rb +59 -0
- data/spec/rails_app/config/boot.rb +6 -0
- data/spec/rails_app/config/database.yml +25 -0
- data/spec/rails_app/config/environment.rb +5 -0
- data/spec/rails_app/config/environments/development.rb +37 -0
- data/spec/rails_app/config/environments/production.rb +67 -0
- data/spec/rails_app/config/environments/test.rb +37 -0
- data/spec/rails_app/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/rails_app/config/initializers/inflections.rb +15 -0
- data/spec/rails_app/config/initializers/mime_types.rb +5 -0
- data/spec/rails_app/config/initializers/secret_token.rb +7 -0
- data/spec/rails_app/config/initializers/session_store.rb +8 -0
- data/spec/rails_app/config/initializers/wrap_parameters.rb +14 -0
- data/spec/rails_app/config/locales/en.yml +5 -0
- data/spec/rails_app/config/routes.rb +58 -0
- data/spec/rails_app/config.ru +4 -0
- data/spec/rails_app/db/seeds.rb +7 -0
- data/spec/rails_app/lib/assets/.gitkeep +0 -0
- data/spec/rails_app/lib/tasks/.gitkeep +0 -0
- data/spec/rails_app/log/.gitkeep +0 -0
- data/spec/rails_app/public/404.html +26 -0
- data/spec/rails_app/public/422.html +26 -0
- data/spec/rails_app/public/500.html +25 -0
- data/spec/rails_app/public/favicon.ico +0 -0
- data/spec/rails_app/public/index.html +241 -0
- data/spec/rails_app/public/robots.txt +5 -0
- data/spec/rails_app/script/rails +6 -0
- data/spec/rails_app/test/fixtures/.gitkeep +0 -0
- data/spec/rails_app/test/functional/.gitkeep +0 -0
- data/spec/rails_app/test/integration/.gitkeep +0 -0
- data/spec/rails_app/test/performance/browsing_test.rb +12 -0
- data/spec/rails_app/test/test_helper.rb +13 -0
- data/spec/rails_app/test/unit/.gitkeep +0 -0
- data/spec/rails_app/vendor/assets/javascripts/.gitkeep +0 -0
- data/spec/rails_app/vendor/assets/stylesheets/.gitkeep +0 -0
- data/spec/rails_app/vendor/plugins/.gitkeep +0 -0
- data/spec/schema.rb +52 -0
- data/spec/spec_helper.rb +19 -0
- metadata +232 -0
data/.document
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm 1.9.3@counter_culture
|
data/Gemfile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
# Add dependencies required to use your gem here.
|
3
|
+
# Example:
|
4
|
+
# gem "activesupport", ">= 2.3.5"
|
5
|
+
|
6
|
+
# Add dependencies to develop your gem here.
|
7
|
+
# Include everything needed to run rake, tests, features, etc.
|
8
|
+
group :development, :test do
|
9
|
+
gem "rails"
|
10
|
+
gem "rspec", "~> 2.10.0"
|
11
|
+
gem "after_commit_action"
|
12
|
+
end
|
13
|
+
|
14
|
+
group :development do
|
15
|
+
gem "rdoc", "~> 3.12"
|
16
|
+
gem "bundler", "~> 1.2.0.pre"
|
17
|
+
gem "jeweler", "~> 1.8.3"
|
18
|
+
end
|
19
|
+
|
20
|
+
group :test do
|
21
|
+
gem "sqlite3"
|
22
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
actionmailer (3.2.3)
|
5
|
+
actionpack (= 3.2.3)
|
6
|
+
mail (~> 2.4.4)
|
7
|
+
actionpack (3.2.3)
|
8
|
+
activemodel (= 3.2.3)
|
9
|
+
activesupport (= 3.2.3)
|
10
|
+
builder (~> 3.0.0)
|
11
|
+
erubis (~> 2.7.0)
|
12
|
+
journey (~> 1.0.1)
|
13
|
+
rack (~> 1.4.0)
|
14
|
+
rack-cache (~> 1.2)
|
15
|
+
rack-test (~> 0.6.1)
|
16
|
+
sprockets (~> 2.1.2)
|
17
|
+
activemodel (3.2.3)
|
18
|
+
activesupport (= 3.2.3)
|
19
|
+
builder (~> 3.0.0)
|
20
|
+
activerecord (3.2.3)
|
21
|
+
activemodel (= 3.2.3)
|
22
|
+
activesupport (= 3.2.3)
|
23
|
+
arel (~> 3.0.2)
|
24
|
+
tzinfo (~> 0.3.29)
|
25
|
+
activeresource (3.2.3)
|
26
|
+
activemodel (= 3.2.3)
|
27
|
+
activesupport (= 3.2.3)
|
28
|
+
activesupport (3.2.3)
|
29
|
+
i18n (~> 0.6)
|
30
|
+
multi_json (~> 1.0)
|
31
|
+
after_commit_action (0.1.3)
|
32
|
+
activerecord (>= 3.0.0)
|
33
|
+
arel (3.0.2)
|
34
|
+
builder (3.0.0)
|
35
|
+
diff-lcs (1.1.3)
|
36
|
+
erubis (2.7.0)
|
37
|
+
git (1.2.5)
|
38
|
+
hike (1.2.1)
|
39
|
+
i18n (0.6.0)
|
40
|
+
jeweler (1.8.3)
|
41
|
+
bundler (~> 1.0)
|
42
|
+
git (>= 1.2.5)
|
43
|
+
rake
|
44
|
+
rdoc
|
45
|
+
journey (1.0.3)
|
46
|
+
json (1.7.3)
|
47
|
+
mail (2.4.4)
|
48
|
+
i18n (>= 0.4.0)
|
49
|
+
mime-types (~> 1.16)
|
50
|
+
treetop (~> 1.4.8)
|
51
|
+
mime-types (1.18)
|
52
|
+
multi_json (1.3.5)
|
53
|
+
polyglot (0.3.3)
|
54
|
+
rack (1.4.1)
|
55
|
+
rack-cache (1.2)
|
56
|
+
rack (>= 0.4)
|
57
|
+
rack-ssl (1.3.2)
|
58
|
+
rack
|
59
|
+
rack-test (0.6.1)
|
60
|
+
rack (>= 1.0)
|
61
|
+
rails (3.2.3)
|
62
|
+
actionmailer (= 3.2.3)
|
63
|
+
actionpack (= 3.2.3)
|
64
|
+
activerecord (= 3.2.3)
|
65
|
+
activeresource (= 3.2.3)
|
66
|
+
activesupport (= 3.2.3)
|
67
|
+
bundler (~> 1.0)
|
68
|
+
railties (= 3.2.3)
|
69
|
+
railties (3.2.3)
|
70
|
+
actionpack (= 3.2.3)
|
71
|
+
activesupport (= 3.2.3)
|
72
|
+
rack-ssl (~> 1.3.2)
|
73
|
+
rake (>= 0.8.7)
|
74
|
+
rdoc (~> 3.4)
|
75
|
+
thor (~> 0.14.6)
|
76
|
+
rake (0.9.2.2)
|
77
|
+
rdoc (3.12)
|
78
|
+
json (~> 1.4)
|
79
|
+
rspec (2.10.0)
|
80
|
+
rspec-core (~> 2.10.0)
|
81
|
+
rspec-expectations (~> 2.10.0)
|
82
|
+
rspec-mocks (~> 2.10.0)
|
83
|
+
rspec-core (2.10.1)
|
84
|
+
rspec-expectations (2.10.0)
|
85
|
+
diff-lcs (~> 1.1.3)
|
86
|
+
rspec-mocks (2.10.1)
|
87
|
+
sprockets (2.1.3)
|
88
|
+
hike (~> 1.2)
|
89
|
+
rack (~> 1.0)
|
90
|
+
tilt (~> 1.1, != 1.3.0)
|
91
|
+
sqlite3 (1.3.6)
|
92
|
+
thor (0.14.6)
|
93
|
+
tilt (1.3.3)
|
94
|
+
treetop (1.4.10)
|
95
|
+
polyglot
|
96
|
+
polyglot (>= 0.3.1)
|
97
|
+
tzinfo (0.3.33)
|
98
|
+
|
99
|
+
PLATFORMS
|
100
|
+
ruby
|
101
|
+
|
102
|
+
DEPENDENCIES
|
103
|
+
after_commit_action
|
104
|
+
bundler (~> 1.2.0.pre)
|
105
|
+
jeweler (~> 1.8.3)
|
106
|
+
rails
|
107
|
+
rdoc (~> 3.12)
|
108
|
+
rspec (~> 2.10.0)
|
109
|
+
sqlite3
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2012 Magnus von Koeller
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# counter_culture
|
2
|
+
|
3
|
+
Turbo-charged counter caches for your Rails app. Huge improvements over the Rails standard counter caches:
|
4
|
+
|
5
|
+
* Updates counter cache when values change, not just when creating and destroying
|
6
|
+
* Supports counter caches through multiple levels of relations
|
7
|
+
* Supports dynamic column names, making it possible to split up the counter cache for different types of objects
|
8
|
+
* Executes counter updates after the commit, avoiding [deadlocks](http://mina.naguib.ca/blog/2010/11/22/postgresql-foreign-key-deadlocks.html)
|
9
|
+
|
10
|
+
## Contributing to counter_culture
|
11
|
+
|
12
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
|
13
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
|
14
|
+
* Fork the project.
|
15
|
+
* Start a feature/bugfix branch.
|
16
|
+
* Commit and push until you are happy with your contribution.
|
17
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
18
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
19
|
+
|
20
|
+
## Copyright
|
21
|
+
|
22
|
+
Copyright (c) 2012 BestVendor. See LICENSE.txt for further details.
|
data/Rakefile
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
begin
|
6
|
+
Bundler.setup(:default, :development)
|
7
|
+
rescue Bundler::BundlerError => e
|
8
|
+
$stderr.puts e.message
|
9
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
10
|
+
exit e.status_code
|
11
|
+
end
|
12
|
+
require 'rake'
|
13
|
+
|
14
|
+
require 'jeweler'
|
15
|
+
Jeweler::Tasks.new do |gem|
|
16
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
17
|
+
gem.name = "counter_culture"
|
18
|
+
gem.homepage = "http://github.com/bestvendor/counter_culture"
|
19
|
+
gem.license = "MIT"
|
20
|
+
gem.summary = %Q{Turbo-charged counter caches for your Rails app.}
|
21
|
+
gem.description = %Q{counter_culture provides turbo-charged counter caches that are kept up-to-date not just on create and destroy, that support multiple levels of indirection through relationships, allow dynamic column names and that avoid deadlocks by updating in the after_commit callback.}
|
22
|
+
gem.email = "magnus@vonkoeller.de"
|
23
|
+
gem.authors = ["Magnus von Koeller"]
|
24
|
+
|
25
|
+
gem.add_dependency 'after_commit_action', '~> 0.1.3'
|
26
|
+
end
|
27
|
+
Jeweler::RubygemsDotOrgTasks.new
|
28
|
+
|
29
|
+
require 'rspec/core'
|
30
|
+
require 'rspec/core/rake_task'
|
31
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
32
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
33
|
+
end
|
34
|
+
|
35
|
+
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
36
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
37
|
+
spec.rcov = true
|
38
|
+
end
|
39
|
+
|
40
|
+
task :default => :spec
|
41
|
+
|
42
|
+
require 'rdoc/task'
|
43
|
+
Rake::RDocTask.new do |rdoc|
|
44
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
45
|
+
|
46
|
+
rdoc.rdoc_dir = 'rdoc'
|
47
|
+
rdoc.title = "counter_culture #{version}"
|
48
|
+
rdoc.rdoc_files.include('README*')
|
49
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
50
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.3
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = "counter_culture"
|
8
|
+
s.version = "0.1.3"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Magnus von Koeller"]
|
12
|
+
s.date = "2012-05-30"
|
13
|
+
s.description = "counter_culture provides turbo-charged counter caches that are kept up-to-date not just on create and destroy, that support multiple levels of indirection through relationships, allow dynamic column names and that avoid deadlocks by updating in the after_commit callback."
|
14
|
+
s.email = "magnus@vonkoeller.de"
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE.txt",
|
17
|
+
"README.md"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".document",
|
21
|
+
".rspec",
|
22
|
+
".rvmrc",
|
23
|
+
"Gemfile",
|
24
|
+
"Gemfile.lock",
|
25
|
+
"LICENSE.txt",
|
26
|
+
"README.md",
|
27
|
+
"Rakefile",
|
28
|
+
"VERSION",
|
29
|
+
"counter_culture.gemspec",
|
30
|
+
"lib/counter_culture.rb",
|
31
|
+
"spec/counter_culture_spec.rb",
|
32
|
+
"spec/models/company.rb",
|
33
|
+
"spec/models/industry.rb",
|
34
|
+
"spec/models/product.rb",
|
35
|
+
"spec/models/review.rb",
|
36
|
+
"spec/models/user.rb",
|
37
|
+
"spec/rails_app/.gitignore",
|
38
|
+
"spec/rails_app/Gemfile",
|
39
|
+
"spec/rails_app/README.rdoc",
|
40
|
+
"spec/rails_app/Rakefile",
|
41
|
+
"spec/rails_app/app/assets/images/rails.png",
|
42
|
+
"spec/rails_app/app/assets/javascripts/application.js",
|
43
|
+
"spec/rails_app/app/assets/stylesheets/application.css",
|
44
|
+
"spec/rails_app/app/controllers/application_controller.rb",
|
45
|
+
"spec/rails_app/app/helpers/application_helper.rb",
|
46
|
+
"spec/rails_app/app/mailers/.gitkeep",
|
47
|
+
"spec/rails_app/app/models/.gitkeep",
|
48
|
+
"spec/rails_app/app/views/layouts/application.html.erb",
|
49
|
+
"spec/rails_app/config.ru",
|
50
|
+
"spec/rails_app/config/application.rb",
|
51
|
+
"spec/rails_app/config/boot.rb",
|
52
|
+
"spec/rails_app/config/database.yml",
|
53
|
+
"spec/rails_app/config/environment.rb",
|
54
|
+
"spec/rails_app/config/environments/development.rb",
|
55
|
+
"spec/rails_app/config/environments/production.rb",
|
56
|
+
"spec/rails_app/config/environments/test.rb",
|
57
|
+
"spec/rails_app/config/initializers/backtrace_silencers.rb",
|
58
|
+
"spec/rails_app/config/initializers/inflections.rb",
|
59
|
+
"spec/rails_app/config/initializers/mime_types.rb",
|
60
|
+
"spec/rails_app/config/initializers/secret_token.rb",
|
61
|
+
"spec/rails_app/config/initializers/session_store.rb",
|
62
|
+
"spec/rails_app/config/initializers/wrap_parameters.rb",
|
63
|
+
"spec/rails_app/config/locales/en.yml",
|
64
|
+
"spec/rails_app/config/routes.rb",
|
65
|
+
"spec/rails_app/db/seeds.rb",
|
66
|
+
"spec/rails_app/lib/assets/.gitkeep",
|
67
|
+
"spec/rails_app/lib/tasks/.gitkeep",
|
68
|
+
"spec/rails_app/log/.gitkeep",
|
69
|
+
"spec/rails_app/public/404.html",
|
70
|
+
"spec/rails_app/public/422.html",
|
71
|
+
"spec/rails_app/public/500.html",
|
72
|
+
"spec/rails_app/public/favicon.ico",
|
73
|
+
"spec/rails_app/public/index.html",
|
74
|
+
"spec/rails_app/public/robots.txt",
|
75
|
+
"spec/rails_app/script/rails",
|
76
|
+
"spec/rails_app/test/fixtures/.gitkeep",
|
77
|
+
"spec/rails_app/test/functional/.gitkeep",
|
78
|
+
"spec/rails_app/test/integration/.gitkeep",
|
79
|
+
"spec/rails_app/test/performance/browsing_test.rb",
|
80
|
+
"spec/rails_app/test/test_helper.rb",
|
81
|
+
"spec/rails_app/test/unit/.gitkeep",
|
82
|
+
"spec/rails_app/vendor/assets/javascripts/.gitkeep",
|
83
|
+
"spec/rails_app/vendor/assets/stylesheets/.gitkeep",
|
84
|
+
"spec/rails_app/vendor/plugins/.gitkeep",
|
85
|
+
"spec/schema.rb",
|
86
|
+
"spec/spec_helper.rb"
|
87
|
+
]
|
88
|
+
s.homepage = "http://github.com/bestvendor/counter_culture"
|
89
|
+
s.licenses = ["MIT"]
|
90
|
+
s.require_paths = ["lib"]
|
91
|
+
s.rubygems_version = "1.8.21"
|
92
|
+
s.summary = "Turbo-charged counter caches for your Rails app."
|
93
|
+
|
94
|
+
if s.respond_to? :specification_version then
|
95
|
+
s.specification_version = 3
|
96
|
+
|
97
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
98
|
+
s.add_development_dependency(%q<rails>, [">= 0"])
|
99
|
+
s.add_development_dependency(%q<rspec>, ["~> 2.10.0"])
|
100
|
+
s.add_development_dependency(%q<after_commit_action>, [">= 0"])
|
101
|
+
s.add_development_dependency(%q<rdoc>, ["~> 3.12"])
|
102
|
+
s.add_development_dependency(%q<bundler>, ["~> 1.2.0.pre"])
|
103
|
+
s.add_development_dependency(%q<jeweler>, ["~> 1.8.3"])
|
104
|
+
s.add_runtime_dependency(%q<after_commit_action>, ["~> 0.1.3"])
|
105
|
+
else
|
106
|
+
s.add_dependency(%q<rails>, [">= 0"])
|
107
|
+
s.add_dependency(%q<rspec>, ["~> 2.10.0"])
|
108
|
+
s.add_dependency(%q<after_commit_action>, [">= 0"])
|
109
|
+
s.add_dependency(%q<rdoc>, ["~> 3.12"])
|
110
|
+
s.add_dependency(%q<bundler>, ["~> 1.2.0.pre"])
|
111
|
+
s.add_dependency(%q<jeweler>, ["~> 1.8.3"])
|
112
|
+
s.add_dependency(%q<after_commit_action>, ["~> 0.1.3"])
|
113
|
+
end
|
114
|
+
else
|
115
|
+
s.add_dependency(%q<rails>, [">= 0"])
|
116
|
+
s.add_dependency(%q<rspec>, ["~> 2.10.0"])
|
117
|
+
s.add_dependency(%q<after_commit_action>, [">= 0"])
|
118
|
+
s.add_dependency(%q<rdoc>, ["~> 3.12"])
|
119
|
+
s.add_dependency(%q<bundler>, ["~> 1.2.0.pre"])
|
120
|
+
s.add_dependency(%q<jeweler>, ["~> 1.8.3"])
|
121
|
+
s.add_dependency(%q<after_commit_action>, ["~> 0.1.3"])
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
@@ -0,0 +1,260 @@
|
|
1
|
+
require 'after_commit_action'
|
2
|
+
|
3
|
+
module CounterCulture
|
4
|
+
|
5
|
+
module ActiveRecord
|
6
|
+
|
7
|
+
def self.included(base)
|
8
|
+
# also add class methods to ActiveRecord::Base
|
9
|
+
base.extend ClassMethods
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
# this holds all configuration data
|
14
|
+
attr_reader :after_commit_counter_cache
|
15
|
+
|
16
|
+
# called to configure counter caches
|
17
|
+
def counter_culture(relation, options = {})
|
18
|
+
unless @after_commit_counter_cache
|
19
|
+
# initialize callbacks only once
|
20
|
+
after_create :_update_counts_after_create
|
21
|
+
after_destroy :_update_counts_after_destroy
|
22
|
+
after_update :_update_counts_after_update
|
23
|
+
|
24
|
+
# we keep a list of all counter caches we must maintain
|
25
|
+
@after_commit_counter_cache = []
|
26
|
+
end
|
27
|
+
|
28
|
+
# add the current information to our list
|
29
|
+
@after_commit_counter_cache<< {
|
30
|
+
:relation => relation.is_a?(Enumerable) ? relation : [relation],
|
31
|
+
:counter_cache_name => (options[:column_name] || "#{name.tableize}_count"),
|
32
|
+
:column_names => options[:column_names],
|
33
|
+
:foreign_key_values => options[:foreign_key_values]
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
# checks all of the declared counter caches on this class for correctnes based
|
38
|
+
# on original data; if the counter cache is incorrect, sets it to the correct
|
39
|
+
# count
|
40
|
+
#
|
41
|
+
# returns: a list of fixed record as an array of hashes of the form:
|
42
|
+
# { :entity => which model the count was fixed on,
|
43
|
+
# :id => the id of the model that had the incorrect count,
|
44
|
+
# :what => which column contained the incorrect count,
|
45
|
+
# :wrong => the previously saved, incorrect count,
|
46
|
+
# :right => the newly fixed, correct count }
|
47
|
+
#
|
48
|
+
def counter_culture_fix_counts
|
49
|
+
fixed = []
|
50
|
+
@after_commit_counter_cache.map do |hash|
|
51
|
+
# which class does this relation ultimately point to? that's where we have to start
|
52
|
+
klass = relation_klass(hash[:relation])
|
53
|
+
|
54
|
+
# we are only interested in the id and the count of related objects (that's this class itself)
|
55
|
+
query = klass.select("#{klass.table_name}.id, COUNT(#{self.table_name}.id) AS count")
|
56
|
+
query = query.group("#{klass.table_name}.id")
|
57
|
+
|
58
|
+
# if we're provided a custom set of column names with conditions, use them; just use the
|
59
|
+
# column name otherwise
|
60
|
+
raise "Must provide :column_names option for relation #{hash[:relation].inspect} when :counter_cache_name is a Proc" if hash[:counter_cache_name].is_a?(Proc) && !hash[:column_names]
|
61
|
+
column_names = hash[:column_names] || {nil => hash[:counter_cache_name]}
|
62
|
+
raise ":column_names must be a Hash of conditions and column names" unless column_names.is_a?(Hash)
|
63
|
+
|
64
|
+
# iterate over all the possible counter cache column names
|
65
|
+
column_names.each do |where, column_name|
|
66
|
+
# if there are additional conditions, add them here
|
67
|
+
counts = query.where(where)
|
68
|
+
|
69
|
+
# we need to work our way back from the end-point of the relation to this class itself;
|
70
|
+
# make a list of arrays pointing to the second-to-last, third-to-last, etc.
|
71
|
+
reverse_relation = []
|
72
|
+
(1..hash[:relation].length).to_a.reverse.each {|i| reverse_relation<< hash[:relation][0,i] }
|
73
|
+
|
74
|
+
# we need to join together tables until we get back to the table this class itself
|
75
|
+
# lives in
|
76
|
+
reverse_relation.each do |cur_relation|
|
77
|
+
reflect = relation_reflect(cur_relation)
|
78
|
+
counts = counts.joins("JOIN #{reflect.active_record.table_name} ON #{reflect.table_name}.id = #{reflect.active_record.table_name}.#{reflect.foreign_key}")
|
79
|
+
end
|
80
|
+
# and then we collect the counts in an id => count hash
|
81
|
+
counts = counts.inject({}){|memo, model| memo[model.id] = model.count.to_i; memo}
|
82
|
+
|
83
|
+
# now that we know what the correct counts are, we need to iterate over all instances
|
84
|
+
# and check whether the count is correct; if not, we correct it
|
85
|
+
klass.find_each do |model|
|
86
|
+
if model.send(column_name) != counts[model.id].to_i
|
87
|
+
# keep track of what we fixed, e.g. for a notification email
|
88
|
+
fixed<< {
|
89
|
+
:entity => klass.name,
|
90
|
+
:id => model.id,
|
91
|
+
:what => column_name,
|
92
|
+
:wrong => model.send(column_name),
|
93
|
+
:right => counts[model.id]
|
94
|
+
}
|
95
|
+
# use update_all because it's faster and because a fixed counter-cache shouldn't
|
96
|
+
# update the timestamp
|
97
|
+
klass.update_all "#{column_name} = #{counts[model.id].to_i}", "id = #{model.id}"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
return fixed
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
# gets the reflect object on the given relation
|
108
|
+
#
|
109
|
+
# relation: a symbol or array of symbols; specifies the relation
|
110
|
+
# that has the counter cache column
|
111
|
+
def relation_reflect(relation)
|
112
|
+
relation = relation.is_a?(Enumerable) ? relation.dup : [relation]
|
113
|
+
|
114
|
+
# go from one relation to the next until we hit the last reflect object
|
115
|
+
klass = self
|
116
|
+
while relation.size > 0
|
117
|
+
cur_relation = relation.shift
|
118
|
+
reflect = klass.reflect_on_association(cur_relation)
|
119
|
+
raise "No relation #{cur_relation} on #{klass.name}" if reflect.nil?
|
120
|
+
klass = reflect.klass
|
121
|
+
end
|
122
|
+
|
123
|
+
return reflect
|
124
|
+
end
|
125
|
+
|
126
|
+
# gets the class of the given relation
|
127
|
+
#
|
128
|
+
# relation: a symbol or array of symbols; specifies the relation
|
129
|
+
# that has the counter cache column
|
130
|
+
def relation_klass(relation)
|
131
|
+
relation_reflect(relation).klass
|
132
|
+
end
|
133
|
+
|
134
|
+
# gets the foreign key name of the given relation
|
135
|
+
#
|
136
|
+
# relation: a symbol or array of symbols; specifies the relation
|
137
|
+
# that has the counter cache column
|
138
|
+
def relation_foreign_key(relation)
|
139
|
+
relation_reflect(relation).foreign_key
|
140
|
+
end
|
141
|
+
|
142
|
+
# gets the foreign key name of the relation. will look at the first
|
143
|
+
# level only -- i.e., if passed an array will consider only its
|
144
|
+
# first element
|
145
|
+
#
|
146
|
+
# relation: a symbol or array of symbols; specifies the relation
|
147
|
+
# that has the counter cache column
|
148
|
+
def first_level_relation_foreign_key(relation)
|
149
|
+
relation = relation.first if relation.is_a?(Enumerable)
|
150
|
+
relation_reflect(relation).foreign_key
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
154
|
+
|
155
|
+
private
|
156
|
+
# called by after_create callback
|
157
|
+
def _update_counts_after_create
|
158
|
+
self.class.after_commit_counter_cache.each do |hash|
|
159
|
+
# increment counter cache
|
160
|
+
change_counter_cache(true, hash)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# called by after_destroy callback
|
165
|
+
def _update_counts_after_destroy
|
166
|
+
self.class.after_commit_counter_cache.each do |hash|
|
167
|
+
# decrement counter cache
|
168
|
+
change_counter_cache(false, hash)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# called by after_update callback
|
173
|
+
def _update_counts_after_update
|
174
|
+
self.class.after_commit_counter_cache.each do |hash|
|
175
|
+
# only update counter caches if the foreign key changed
|
176
|
+
if send("#{first_level_relation_foreign_key(hash[:relation])}_changed?")
|
177
|
+
# increment the counter cache of the new value
|
178
|
+
change_counter_cache(true, hash)
|
179
|
+
# decrement the counter cache of the old value
|
180
|
+
change_counter_cache(false, hash, true)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
# increments or decrements a counter cache
|
186
|
+
#
|
187
|
+
# increment: true to increment, false to decrement
|
188
|
+
# hash:
|
189
|
+
# :relation => which relation to increment the count on,
|
190
|
+
# :counter_cache_name => the column name of the counter cache
|
191
|
+
# was: whether to get the current value or the old value of the
|
192
|
+
# first part of the relation
|
193
|
+
def change_counter_cache(increment, hash, was = false)
|
194
|
+
# default to the current foreign key value
|
195
|
+
id_to_change = foreign_key_value(hash[:relation], was)
|
196
|
+
# allow overwriting of foreign key value by the caller
|
197
|
+
id_to_change = hash[:foreign_key_values].call(id_to_change) if hash[:foreign_key_values]
|
198
|
+
if id_to_change
|
199
|
+
execute_after_commit do
|
200
|
+
# increment or decrement?
|
201
|
+
method = increment ? :increment_counter : :decrement_counter
|
202
|
+
|
203
|
+
# figure out what the column name is
|
204
|
+
if hash[:counter_cache_name].is_a? Proc
|
205
|
+
# dynamic column name -- call the Proc
|
206
|
+
counter_cache_name = hash[:counter_cache_name].call(self)
|
207
|
+
else
|
208
|
+
# static column name
|
209
|
+
counter_cache_name = hash[:counter_cache_name]
|
210
|
+
end
|
211
|
+
|
212
|
+
# do it!
|
213
|
+
relation_klass(hash[:relation]).send(method, counter_cache_name, id_to_change)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
# gets the value of the foreign key on the given relation
|
219
|
+
#
|
220
|
+
# relation: a symbol or array of symbols; specifies the relation
|
221
|
+
# that has the counter cache column
|
222
|
+
# was: whether to get the current or past value from ActiveRecord;
|
223
|
+
# pass true to get the past value, false or nothing to get the
|
224
|
+
# current value
|
225
|
+
def foreign_key_value(relation, was = false)
|
226
|
+
relation = relation.is_a?(Enumerable) ? relation.dup : [relation]
|
227
|
+
if was
|
228
|
+
first = relation.shift
|
229
|
+
foreign_key_value = send("#{relation_foreign_key(first)}_was")
|
230
|
+
value = relation_klass(first).find(foreign_key_value) if foreign_key_value
|
231
|
+
else
|
232
|
+
value = self
|
233
|
+
end
|
234
|
+
while !value.nil? && relation.size > 0
|
235
|
+
value = value.send(relation.shift)
|
236
|
+
end
|
237
|
+
return value.try(:id)
|
238
|
+
end
|
239
|
+
|
240
|
+
def relation_klass(relation)
|
241
|
+
self.class.send :relation_klass, relation
|
242
|
+
end
|
243
|
+
|
244
|
+
def relation_reflect(relation)
|
245
|
+
self.class.send :relation_reflect, relation
|
246
|
+
end
|
247
|
+
|
248
|
+
def relation_foreign_key(relation)
|
249
|
+
self.class.send :relation_foreign_key, relation
|
250
|
+
end
|
251
|
+
|
252
|
+
def first_level_relation_foreign_key(relation)
|
253
|
+
self.class.send :first_level_relation_foreign_key, relation
|
254
|
+
end
|
255
|
+
|
256
|
+
end
|
257
|
+
|
258
|
+
# extend ActiveRecord with our own code here
|
259
|
+
::ActiveRecord::Base.send :include, ActiveRecord
|
260
|
+
end
|