counter_culture 1.4.0 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +1 -1
- data/VERSION +1 -1
- data/counter_culture.gemspec +5 -2
- data/lib/counter_culture/counter.rb +82 -11
- data/lib/counter_culture/extensions.rb +3 -8
- data/lib/counter_culture/reconciler.rb +136 -87
- data/spec/counter_culture_spec.rb +153 -0
- data/spec/models/poly_employee.rb +3 -0
- data/spec/models/poly_image.rb +15 -0
- data/spec/models/poly_product.rb +4 -0
- data/spec/schema.rb +19 -0
- metadata +4 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 84e93f1182982eccea8a717da0a95ebb3e4cfef8
|
4
|
+
data.tar.gz: 87e88e5de74ddf5c23d7df9ff3273ce3f0b17fdf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0c7286393c38d78c7d59817dfe5dae1c7c05484d3f8c186f3c56b5f0dcaa8c0da3e922aad3395ea94c08cc638536633dfbdd8c3338edf9fd17927eea029ff024
|
7
|
+
data.tar.gz: 4872ec4223bc8a7ebdc0ca31852e0e7358121dc32f49c8302f744d451b5e32d3a17fcf972dfa8b9cd8be3c21753c2aea38cfec8b2e2fac86ae47cc5a4b1c4079
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -295,7 +295,7 @@ Manually populating counter caches with dynamically over-written foreign keys (`
|
|
295
295
|
|
296
296
|
#### Polymorphic associations
|
297
297
|
|
298
|
-
counter_culture
|
298
|
+
counter_culture now supports polymorphic associations of one level only.
|
299
299
|
|
300
300
|
## Contributing to counter_culture
|
301
301
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.
|
1
|
+
1.5.0
|
data/counter_culture.gemspec
CHANGED
@@ -2,11 +2,11 @@
|
|
2
2
|
# DO NOT EDIT THIS FILE DIRECTLY
|
3
3
|
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
4
|
# -*- encoding: utf-8 -*-
|
5
|
-
# stub: counter_culture 1.
|
5
|
+
# stub: counter_culture 1.5.0 ruby lib
|
6
6
|
|
7
7
|
Gem::Specification.new do |s|
|
8
8
|
s.name = "counter_culture".freeze
|
9
|
-
s.version = "1.
|
9
|
+
s.version = "1.5.0"
|
10
10
|
|
11
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
|
12
12
|
s.require_paths = ["lib".freeze]
|
@@ -48,6 +48,9 @@ Gem::Specification.new do |s|
|
|
48
48
|
"spec/models/has_string_id.rb",
|
49
49
|
"spec/models/industry.rb",
|
50
50
|
"spec/models/person.rb",
|
51
|
+
"spec/models/poly_employee.rb",
|
52
|
+
"spec/models/poly_image.rb",
|
53
|
+
"spec/models/poly_product.rb",
|
51
54
|
"spec/models/post.rb",
|
52
55
|
"spec/models/post_comment.rb",
|
53
56
|
"spec/models/product.rb",
|
@@ -62,8 +62,9 @@ module CounterCulture
|
|
62
62
|
end
|
63
63
|
end
|
64
64
|
|
65
|
-
klass = relation_klass(relation)
|
66
|
-
|
65
|
+
klass = relation_klass(relation, source: obj, was: options[:was])
|
66
|
+
primary_key = relation_primary_key(relation, source: obj, was: options[:was])
|
67
|
+
klass.where(primary_key => id_to_change).update_all updates.join(', ')
|
67
68
|
end
|
68
69
|
end
|
69
70
|
end
|
@@ -112,15 +113,19 @@ module CounterCulture
|
|
112
113
|
if was
|
113
114
|
first = relation.shift
|
114
115
|
foreign_key_value = attribute_was(obj, relation_foreign_key(first))
|
115
|
-
klass = relation_klass(first)
|
116
|
-
|
116
|
+
klass = relation_klass(first, source: obj, was: was)
|
117
|
+
if foreign_key_value
|
118
|
+
value = klass.where(
|
119
|
+
"#{klass.table_name}.#{relation_primary_key(first, source: obj, was: was)} = ?",
|
120
|
+
foreign_key_value).first
|
121
|
+
end
|
117
122
|
else
|
118
123
|
value = obj
|
119
124
|
end
|
120
125
|
while !value.nil? && relation.size > 0
|
121
126
|
value = value.send(relation.shift)
|
122
127
|
end
|
123
|
-
return value.try(relation_primary_key(first_relation).to_sym)
|
128
|
+
return value.try(relation_primary_key(first_relation, source: obj, was: was).to_sym)
|
124
129
|
end
|
125
130
|
|
126
131
|
# gets the reflect object on the given relation
|
@@ -136,7 +141,13 @@ module CounterCulture
|
|
136
141
|
cur_relation = relation.shift
|
137
142
|
reflect = klass.reflect_on_association(cur_relation)
|
138
143
|
raise "No relation #{cur_relation} on #{klass.name}" if reflect.nil?
|
139
|
-
|
144
|
+
|
145
|
+
if relation.size > 0
|
146
|
+
# not necessary to do this at the last link because we won't use
|
147
|
+
# klass again. not calling this avoids the following causing an
|
148
|
+
# exception in the now-supported one-level polymorphic counter cache
|
149
|
+
klass = reflect.klass
|
150
|
+
end
|
140
151
|
end
|
141
152
|
|
142
153
|
return reflect
|
@@ -146,8 +157,51 @@ module CounterCulture
|
|
146
157
|
#
|
147
158
|
# relation: a symbol or array of symbols; specifies the relation
|
148
159
|
# that has the counter cache column
|
149
|
-
|
150
|
-
|
160
|
+
# source [optional]: the source object,
|
161
|
+
# only needed for polymorphic associations,
|
162
|
+
# probably only works with a single relation (symbol, or array of 1 symbol)
|
163
|
+
# was: boolean
|
164
|
+
# we're actually looking for the old value -- only can change for polymorphic relations
|
165
|
+
def relation_klass(relation, source: nil, was: false)
|
166
|
+
reflect = relation_reflect(relation)
|
167
|
+
if reflect.options.key?(:polymorphic)
|
168
|
+
raise "Can't work out relation's class without being passed object (relation: #{relation}, reflect: #{reflect})" if source.nil?
|
169
|
+
raise "Can't work out polymorhpic relation's class with multiple relations yet" unless (relation.is_a?(Symbol) || relation.length == 1)
|
170
|
+
# this is the column that stores the polymorphic type, aka the class name
|
171
|
+
type_column = reflect.foreign_type.to_sym
|
172
|
+
# so now turn that into the class that we're looking for here
|
173
|
+
if was
|
174
|
+
attribute_was(source, type_column).constantize
|
175
|
+
else
|
176
|
+
source.public_send(type_column).constantize
|
177
|
+
end
|
178
|
+
else
|
179
|
+
reflect.klass
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def first_level_relation_changed?(instance)
|
184
|
+
return true if attribute_changed?(instance, first_level_relation_foreign_key)
|
185
|
+
if polymorphic?
|
186
|
+
return true if attribute_changed?(instance, first_level_relation_foreign_type)
|
187
|
+
end
|
188
|
+
false
|
189
|
+
end
|
190
|
+
|
191
|
+
def attribute_changed?(obj, attr)
|
192
|
+
if Rails.version >= "5.1.0"
|
193
|
+
obj.saved_changes[attr].present?
|
194
|
+
else
|
195
|
+
obj.send(:attribute_changed?, attr)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def polymorphic?
|
200
|
+
is_polymorphic = relation_reflect(relation).options.key?(:polymorphic)
|
201
|
+
if is_polymorphic && !(relation.is_a?(Symbol) || relation.length == 1)
|
202
|
+
raise "Polymorphic associations only supported with one level"
|
203
|
+
end
|
204
|
+
return is_polymorphic
|
151
205
|
end
|
152
206
|
|
153
207
|
# gets the foreign key name of the given relation
|
@@ -162,8 +216,20 @@ module CounterCulture
|
|
162
216
|
#
|
163
217
|
# relation: a symbol or array of symbols; specifies the relation
|
164
218
|
# that has the counter cache column
|
165
|
-
|
166
|
-
|
219
|
+
# source[optional]: the model instance that the relationship is linked from,
|
220
|
+
# only needed for polymorphic associations,
|
221
|
+
# probably only works with a single relation (symbol, or array of 1 symbol)
|
222
|
+
# was: boolean
|
223
|
+
# we're actually looking for the old value -- only can change for polymorphic relations
|
224
|
+
def relation_primary_key(relation, source: nil, was: false)
|
225
|
+
reflect = relation_reflect(relation)
|
226
|
+
klass = nil
|
227
|
+
if reflect.options.key?(:polymorphic)
|
228
|
+
raise "can't handle multiple keys with polymorphic associations" unless (relation.is_a?(Symbol) || relation.length == 1)
|
229
|
+
raise "must specify source for polymorphic associations..." unless source
|
230
|
+
return relation_klass(relation, source: source, was: was).primary_key
|
231
|
+
end
|
232
|
+
reflect.association_primary_key(klass)
|
167
233
|
end
|
168
234
|
|
169
235
|
# gets the foreign key name of the relation. will look at the first
|
@@ -177,6 +243,12 @@ module CounterCulture
|
|
177
243
|
relation_reflect(first_relation).foreign_key
|
178
244
|
end
|
179
245
|
|
246
|
+
def first_level_relation_foreign_type
|
247
|
+
return nil unless polymorphic?
|
248
|
+
first_relation = relation.first if relation.is_a?(Enumerable)
|
249
|
+
relation_reflect(first_relation).foreign_type
|
250
|
+
end
|
251
|
+
|
180
252
|
def previous_model(obj)
|
181
253
|
prev = obj.dup
|
182
254
|
|
@@ -190,7 +262,6 @@ module CounterCulture
|
|
190
262
|
end
|
191
263
|
|
192
264
|
private
|
193
|
-
|
194
265
|
def execute_change_counter_cache(obj, options)
|
195
266
|
if execute_after_commit
|
196
267
|
obj.execute_after_commit { yield }
|
@@ -111,14 +111,9 @@ module CounterCulture
|
|
111
111
|
counter_cache_name_was = counter.counter_cache_name_for(counter.previous_model(self))
|
112
112
|
counter_cache_name = counter.counter_cache_name_for(self)
|
113
113
|
|
114
|
-
if
|
115
|
-
|
116
|
-
|
117
|
-
else
|
118
|
-
foreign_key_changed = attribute_changed?(counter.first_level_relation_foreign_key)
|
119
|
-
delta_column_changed = (counter.delta_column && attribute_changed?(counter.delta_column))
|
120
|
-
end
|
121
|
-
if foreign_key_changed || delta_column_changed || counter_cache_name != counter_cache_name_was
|
114
|
+
if counter.first_level_relation_changed?(self) ||
|
115
|
+
(counter.delta_column && counter.attribute_changed?(self, counter.delta_column)) ||
|
116
|
+
counter_cache_name != counter_cache_name_was
|
122
117
|
|
123
118
|
# increment the counter cache of the new value
|
124
119
|
counter.change_counter_cache(self, :increment => true, :counter_column => counter_cache_name)
|
@@ -5,7 +5,7 @@ module CounterCulture
|
|
5
5
|
class Reconciler
|
6
6
|
attr_reader :counter, :options, :changes
|
7
7
|
|
8
|
-
delegate :model, :relation, :full_primary_key, :relation_reflect, :to => :counter
|
8
|
+
delegate :model, :relation, :full_primary_key, :relation_reflect, :polymorphic?, :to => :counter
|
9
9
|
delegate *CounterCulture::Counter::CONFIG_OPTIONS, :to => :counter
|
10
10
|
|
11
11
|
def initialize(counter, options={})
|
@@ -26,117 +26,166 @@ module CounterCulture
|
|
26
26
|
raise "Fixing counter caches is not supported when :delta_magnitude is a Proc; you may skip this relation with :skip_unsupported => true" if delta_magnitude.is_a?(Proc)
|
27
27
|
end
|
28
28
|
|
29
|
-
|
30
|
-
|
31
|
-
|
29
|
+
associated_model_classes.each do |associated_model_class|
|
30
|
+
Reconciliation.new(counter, changes, options, associated_model_class).perform
|
31
|
+
end
|
32
32
|
|
33
|
-
|
33
|
+
@reconciled = true
|
34
|
+
end
|
34
35
|
|
35
|
-
|
36
|
-
scope = scope.where("#{model.table_name}.deleted_at IS NULL") if model.column_names.include?('deleted_at')
|
36
|
+
private
|
37
37
|
|
38
|
-
|
38
|
+
def associated_model_classes
|
39
|
+
if polymorphic?
|
40
|
+
polymorphic_associated_model_classes
|
41
|
+
else
|
42
|
+
[associated_model_class]
|
43
|
+
end
|
44
|
+
end
|
39
45
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
# specifying that condition regardless to make the syntax less
|
45
|
-
# confusing
|
46
|
-
next unless column_name
|
46
|
+
def polymorphic_associated_model_classes
|
47
|
+
foreign_type_field = relation_reflect(relation).foreign_type
|
48
|
+
model.pluck("DISTINCT #{foreign_type_field}").map(&:constantize)
|
49
|
+
end
|
47
50
|
|
48
|
-
|
49
|
-
|
51
|
+
def associated_model_class
|
52
|
+
counter.relation_klass(counter.relation)
|
53
|
+
end
|
50
54
|
|
51
|
-
# we need to join together tables until we get back to the table this class itself lives in
|
52
|
-
# conditions must also be applied to the join on which we are counting
|
53
|
-
join_clauses.each_with_index do |join,index|
|
54
|
-
if index == join_clauses.size - 1 && where
|
55
|
-
join += " AND (#{model.send(:sanitize_sql_for_conditions, where)})"
|
56
|
-
end
|
57
|
-
counts_query = counts_query.joins(join)
|
58
|
-
end
|
59
55
|
|
60
|
-
|
61
|
-
|
62
|
-
batch_size = options.fetch(:batch_size, CounterCulture.config.batch_size)
|
56
|
+
class Reconciliation
|
57
|
+
attr_reader :counter, :options, :relation_class
|
63
58
|
|
64
|
-
|
65
|
-
|
66
|
-
ActiveRecord::Base.transaction do
|
67
|
-
records.each do |record|
|
68
|
-
count = record.read_attribute('count') || 0
|
69
|
-
next if record.read_attribute(column_name) == count
|
59
|
+
delegate :model, :relation, :full_primary_key, :relation_reflect, :polymorphic?, :to => :counter
|
60
|
+
delegate *CounterCulture::Counter::CONFIG_OPTIONS, :to => :counter
|
70
61
|
|
71
|
-
|
62
|
+
def initialize(counter, changes_holder, options, relation_class)
|
63
|
+
@counter, @options, = counter, options
|
64
|
+
@relation_class = relation_class
|
65
|
+
@changes_holder = changes_holder
|
66
|
+
end
|
72
67
|
|
73
|
-
|
74
|
-
|
68
|
+
def perform
|
69
|
+
# if we're provided a custom set of column names with conditions, use them; just use the
|
70
|
+
# column name otherwise
|
71
|
+
# which class does this relation ultimately point to? that's where we have to start
|
72
|
+
|
73
|
+
scope = relation_class
|
74
|
+
|
75
|
+
# respect the deleted_at column if it exists
|
76
|
+
scope = scope.where("#{model.table_name}.deleted_at IS NULL") if model.column_names.include?('deleted_at')
|
77
|
+
|
78
|
+
counter_column_names = column_names || {nil => counter_cache_name}
|
79
|
+
|
80
|
+
# iterate over all the possible counter cache column names
|
81
|
+
counter_column_names.each do |where, column_name|
|
82
|
+
# if the column name is nil, that means those records don't affect
|
83
|
+
# counts; we don't need to do anything in that case. but we allow
|
84
|
+
# specifying that condition regardless to make the syntax less
|
85
|
+
# confusing
|
86
|
+
next unless column_name
|
87
|
+
|
88
|
+
# select join column and count (from above) as well as cache column ('column_name') for later comparison
|
89
|
+
counts_query = scope.select("#{relation_class.table_name}.#{relation_class.primary_key}, #{relation_class.table_name}.#{relation_reflect(relation).association_primary_key(relation_class)}, #{count_select} AS count, #{relation_class.table_name}.#{column_name}")
|
90
|
+
|
91
|
+
# we need to join together tables until we get back to the table this class itself lives in
|
92
|
+
# conditions must also be applied to the join on which we are counting
|
93
|
+
join_clauses.each_with_index do |join, index|
|
94
|
+
if index == join_clauses.size - 1 && where
|
95
|
+
join += " AND (#{model.send(:sanitize_sql_for_conditions, where)})"
|
75
96
|
end
|
97
|
+
counts_query = counts_query.joins(join)
|
76
98
|
end
|
77
|
-
end
|
78
|
-
end
|
79
99
|
|
80
|
-
|
81
|
-
|
100
|
+
# iterate in batches; otherwise we might run out of memory when there's a lot of
|
101
|
+
# instances and we try to load all their counts at once
|
102
|
+
batch_size = options.fetch(:batch_size, CounterCulture.config.batch_size)
|
82
103
|
|
83
|
-
|
104
|
+
counts_query.group(full_primary_key(relation_class)).find_in_batches(batch_size: batch_size) do |records|
|
105
|
+
# now iterate over all the models and see whether their counts are right
|
106
|
+
ActiveRecord::Base.transaction do
|
107
|
+
records.each do |record|
|
108
|
+
count = record.read_attribute('count') || 0
|
109
|
+
next if record.read_attribute(column_name) == count
|
84
110
|
|
85
|
-
|
86
|
-
def track_change(record, column_name, count)
|
87
|
-
@changes << {
|
88
|
-
:entity => relation_class.name,
|
89
|
-
relation_class.primary_key.to_sym => record.send(relation_class.primary_key),
|
90
|
-
:what => column_name,
|
91
|
-
:wrong => record.send(column_name),
|
92
|
-
:right => count
|
93
|
-
}
|
94
|
-
end
|
111
|
+
track_change(record, column_name, count)
|
95
112
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
@count_select = "COUNT(#{self_table_name}.#{model.primary_key})*#{delta_magnitude}"
|
113
|
+
# use update_all because it's faster and because a fixed counter-cache shouldn't update the timestamp
|
114
|
+
relation_class.where(relation_class.primary_key => record.send(relation_class.primary_key)).update_all(column_name => count)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
103
119
|
end
|
104
|
-
end
|
105
120
|
|
106
|
-
|
107
|
-
|
108
|
-
|
121
|
+
private
|
122
|
+
|
123
|
+
# keep track of what we fixed, e.g. for a notification email
|
124
|
+
def track_change(record, column_name, count)
|
125
|
+
@changes_holder << {
|
126
|
+
:entity => relation_class.name,
|
127
|
+
relation_class.primary_key.to_sym => record.send(relation_class.primary_key),
|
128
|
+
:what => column_name,
|
129
|
+
:wrong => record.send(column_name),
|
130
|
+
:right => count
|
131
|
+
}
|
132
|
+
end
|
109
133
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
134
|
+
def count_select
|
135
|
+
# if a delta column is provided use SUM, otherwise use COUNT
|
136
|
+
return @count_select if @count_select
|
137
|
+
if delta_column
|
138
|
+
@count_select = "SUM(COALESCE(#{self_table_name}.#{delta_column},0))"
|
139
|
+
else
|
140
|
+
@count_select = "COUNT(#{self_table_name}.#{model.primary_key})*#{delta_magnitude}"
|
141
|
+
end
|
115
142
|
end
|
116
|
-
end
|
117
143
|
|
118
|
-
|
119
|
-
|
144
|
+
def self_table_name
|
145
|
+
@self_table_name ||= if relation_class.table_name == model.table_name
|
146
|
+
"#{model.table_name}_#{model.table_name}"
|
147
|
+
else
|
148
|
+
model.table_name
|
149
|
+
end
|
150
|
+
end
|
120
151
|
|
121
|
-
|
122
|
-
|
123
|
-
reverse_relation = (1..relation.length).to_a.reverse.inject([]) {|a,i| a << relation[0,i]; a }
|
152
|
+
def join_clauses
|
153
|
+
return @join_clauses if defined?(@join_clauses)
|
124
154
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
155
|
+
# we need to work our way back from the end-point of the relation to this class itself;
|
156
|
+
# make a list of arrays pointing to the second-to-last, third-to-last, etc.
|
157
|
+
reverse_relation = (1..relation.length).to_a.reverse.inject([]) { |a, i| a << relation[0, i]; a }
|
158
|
+
|
159
|
+
# store joins in an array so that we can later apply column-specific conditions
|
160
|
+
@join_clauses = reverse_relation.map do |cur_relation|
|
161
|
+
reflect = relation_reflect(cur_relation)
|
162
|
+
if relation_class.table_name == reflect.active_record.table_name
|
163
|
+
join_table_name = "#{relation_class.table_name}_#{relation_class.table_name}"
|
164
|
+
else
|
165
|
+
join_table_name = reflect.active_record.table_name
|
166
|
+
end
|
167
|
+
if polymorphic?
|
168
|
+
# NB: polymorphic only supports one level of relation (at present)
|
169
|
+
association_primary_key = reflect.association_primary_key(relation_class)
|
170
|
+
reflect_table_name = relation_class.table_name
|
171
|
+
else
|
172
|
+
association_primary_key = reflect.association_primary_key
|
173
|
+
reflect_table_name = reflect.table_name
|
174
|
+
end
|
175
|
+
|
176
|
+
# join with alias to avoid ambiguous table name with self-referential models:
|
177
|
+
joins_sql = "LEFT JOIN #{reflect.active_record.table_name} AS #{join_table_name} ON #{reflect_table_name}.#{association_primary_key} = #{join_table_name}.#{reflect.foreign_key}"
|
178
|
+
# adds 'type' condition to JOIN clause if the current model is a child in a Single Table Inheritance
|
179
|
+
joins_sql = "#{joins_sql} AND #{reflect.active_record.table_name}.type IN ('#{model.name}')" if reflect.active_record.column_names.include?('type') && !model.descends_from_active_record?
|
180
|
+
if polymorphic?
|
181
|
+
# adds 'type' condition to JOIN clause if the current model is a polymorphic relation
|
182
|
+
# NB only works for one-level relations
|
183
|
+
joins_sql = "#{joins_sql} AND #{reflect.active_record.table_name}.#{reflect.foreign_type} = '#{relation_class.name}'"
|
184
|
+
end
|
185
|
+
joins_sql
|
132
186
|
end
|
133
|
-
# join with alias to avoid ambiguous table name with self-referential models:
|
134
|
-
joins_sql = "LEFT JOIN #{reflect.active_record.table_name} AS #{join_table_name} ON #{reflect.table_name}.#{reflect.association_primary_key} = #{join_table_name}.#{reflect.foreign_key}"
|
135
|
-
# adds 'type' condition to JOIN clause if the current model is a child in a Single Table Inheritance
|
136
|
-
joins_sql = "#{joins_sql} AND #{reflect.active_record.table_name}.type IN ('#{model.name}')" if reflect.active_record.column_names.include?('type') && !model.descends_from_active_record?
|
137
|
-
joins_sql
|
138
187
|
end
|
139
|
-
end
|
140
188
|
|
189
|
+
end
|
141
190
|
end
|
142
191
|
end
|
@@ -1585,4 +1585,157 @@ describe "CounterCulture" do
|
|
1585
1585
|
])
|
1586
1586
|
end
|
1587
1587
|
end
|
1588
|
+
|
1589
|
+
describe "with polymorphic_associations" do
|
1590
|
+
before(:all) do
|
1591
|
+
require 'models/poly_image'
|
1592
|
+
require 'models/poly_employee'
|
1593
|
+
require 'models/poly_product'
|
1594
|
+
end
|
1595
|
+
let(:employee) { PolyEmployee.create(id: 3000) }
|
1596
|
+
let(:product1) { PolyProduct.create() }
|
1597
|
+
let(:product2) { PolyProduct.create() }
|
1598
|
+
let(:special_url) { "http://images.example.com/special.png" }
|
1599
|
+
|
1600
|
+
def mess_up_counts
|
1601
|
+
PolyEmployee.update_all(poly_images_count: 100, poly_images_count_dup: 100, special_poly_images_count: 100)
|
1602
|
+
PolyProduct.update_all(poly_images_count: 100, poly_images_count_dup: 100, special_poly_images_count: 100)
|
1603
|
+
end
|
1604
|
+
|
1605
|
+
describe "default" do
|
1606
|
+
it "increments / decrements counter caches correctly" do
|
1607
|
+
expect(employee.poly_images_count).to eq(0)
|
1608
|
+
expect(product1.poly_images_count).to eq(0)
|
1609
|
+
img1 = PolyImage.create(imageable: employee)
|
1610
|
+
expect(employee.reload.poly_images_count).to eq(1)
|
1611
|
+
expect(product1.reload.poly_images_count).to eq(0)
|
1612
|
+
img2 = PolyImage.create(imageable: product1)
|
1613
|
+
expect(employee.reload.poly_images_count).to eq(1)
|
1614
|
+
expect(product1.reload.poly_images_count).to eq(1)
|
1615
|
+
img3 = PolyImage.create(imageable: product1)
|
1616
|
+
expect(employee.reload.poly_images_count).to eq(1)
|
1617
|
+
expect(product1.reload.poly_images_count).to eq(2)
|
1618
|
+
img3.destroy
|
1619
|
+
expect(employee.reload.poly_images_count).to eq(1)
|
1620
|
+
expect(product1.reload.poly_images_count).to eq(1)
|
1621
|
+
img2.imageable = employee
|
1622
|
+
img2.save!
|
1623
|
+
expect(employee.reload.poly_images_count).to eq(2)
|
1624
|
+
expect(product1.reload.poly_images_count).to eq(0)
|
1625
|
+
end
|
1626
|
+
|
1627
|
+
it "decrements counter caches on update correctly" do
|
1628
|
+
img = PolyImage.create(imageable: product1)
|
1629
|
+
img.imageable = employee
|
1630
|
+
img.save!
|
1631
|
+
expect(product1.reload.poly_images_count).to eq(0)
|
1632
|
+
expect(employee.reload.poly_images_count).to eq(1)
|
1633
|
+
end
|
1634
|
+
|
1635
|
+
it "can fix counts for polymorphic correctly" do
|
1636
|
+
2.times { PolyImage.create(imageable: employee) }
|
1637
|
+
1.times { PolyImage.create(imageable: product1) }
|
1638
|
+
mess_up_counts
|
1639
|
+
|
1640
|
+
PolyImage.counter_culture_fix_counts
|
1641
|
+
|
1642
|
+
expect(product2.reload.poly_images_count).to eq(0)
|
1643
|
+
expect(product1.reload.poly_images_count).to eq(1)
|
1644
|
+
expect(employee.reload.poly_images_count).to eq(2)
|
1645
|
+
end
|
1646
|
+
end
|
1647
|
+
describe "custom column name" do
|
1648
|
+
it "increments counter cache on create" do
|
1649
|
+
expect(employee.poly_images_count_dup).to eq(0)
|
1650
|
+
expect(product1.poly_images_count_dup).to eq(0)
|
1651
|
+
img1 = PolyImage.create(imageable: employee)
|
1652
|
+
expect(employee.reload.poly_images_count_dup).to eq(1)
|
1653
|
+
expect(product1.reload.poly_images_count_dup).to eq(0)
|
1654
|
+
img2 = PolyImage.create(imageable: product1)
|
1655
|
+
expect(employee.reload.poly_images_count_dup).to eq(1)
|
1656
|
+
expect(product1.reload.poly_images_count_dup).to eq(1)
|
1657
|
+
img3 = PolyImage.create(imageable: product1)
|
1658
|
+
expect(employee.reload.poly_images_count_dup).to eq(1)
|
1659
|
+
expect(product1.reload.poly_images_count_dup).to eq(2)
|
1660
|
+
img3.destroy
|
1661
|
+
expect(employee.reload.poly_images_count_dup).to eq(1)
|
1662
|
+
expect(product1.reload.poly_images_count_dup).to eq(1)
|
1663
|
+
img2.imageable = employee
|
1664
|
+
img2.save!
|
1665
|
+
expect(employee.reload.poly_images_count_dup).to eq(2)
|
1666
|
+
expect(product1.reload.poly_images_count_dup).to eq(0)
|
1667
|
+
end
|
1668
|
+
|
1669
|
+
it "decrements counter caches on update correctly" do
|
1670
|
+
img = PolyImage.create(imageable: product1)
|
1671
|
+
img.imageable = employee
|
1672
|
+
img.save!
|
1673
|
+
expect(employee.reload.poly_images_count_dup).to eq(1)
|
1674
|
+
expect(product1.reload.poly_images_count_dup).to eq(0)
|
1675
|
+
end
|
1676
|
+
|
1677
|
+
it "can fix counts for polymorphic correctly" do
|
1678
|
+
2.times { PolyImage.create(imageable: employee) }
|
1679
|
+
1.times { PolyImage.create(imageable: product1) }
|
1680
|
+
mess_up_counts
|
1681
|
+
|
1682
|
+
PolyImage.counter_culture_fix_counts
|
1683
|
+
|
1684
|
+
expect(product2.reload.poly_images_count_dup).to eq(0)
|
1685
|
+
expect(product1.reload.poly_images_count_dup).to eq(1)
|
1686
|
+
expect(employee.reload.poly_images_count_dup).to eq(2)
|
1687
|
+
end
|
1688
|
+
end
|
1689
|
+
describe "conditional counts" do
|
1690
|
+
it "increments counter cache on create" do
|
1691
|
+
expect(employee.special_poly_images_count).to eq(0)
|
1692
|
+
expect(product1.special_poly_images_count).to eq(0)
|
1693
|
+
PolyImage.create(imageable: employee)
|
1694
|
+
expect(employee.reload.special_poly_images_count).to eq(0)
|
1695
|
+
expect(product1.special_poly_images_count).to eq(0)
|
1696
|
+
PolyImage.create(imageable: product1)
|
1697
|
+
expect(employee.reload.special_poly_images_count).to eq(0)
|
1698
|
+
expect(product1.reload.special_poly_images_count).to eq(0)
|
1699
|
+
img1 = PolyImage.create(imageable: employee, url: special_url)
|
1700
|
+
expect(employee.reload.special_poly_images_count).to eq(1)
|
1701
|
+
expect(product1.special_poly_images_count).to eq(0)
|
1702
|
+
img2 = PolyImage.create(imageable: product1, url: special_url)
|
1703
|
+
expect(employee.reload.special_poly_images_count).to eq(1)
|
1704
|
+
expect(product1.reload.special_poly_images_count).to eq(1)
|
1705
|
+
img2.destroy
|
1706
|
+
expect(employee.reload.special_poly_images_count).to eq(1)
|
1707
|
+
expect(product1.reload.special_poly_images_count).to eq(0)
|
1708
|
+
img1.imageable = product1
|
1709
|
+
img1.save!
|
1710
|
+
expect(employee.reload.special_poly_images_count).to eq(0)
|
1711
|
+
expect(product1.reload.special_poly_images_count).to eq(1)
|
1712
|
+
end
|
1713
|
+
|
1714
|
+
it "can fix counts for polymorphic correctly" do
|
1715
|
+
4.times { PolyImage.create(imageable: employee) }
|
1716
|
+
2.times { PolyImage.create(imageable: employee, url: special_url) }
|
1717
|
+
1.times { PolyImage.create(imageable: product1) }
|
1718
|
+
1.times { PolyImage.create(imageable: product1, url: special_url) }
|
1719
|
+
mess_up_counts
|
1720
|
+
|
1721
|
+
PolyImage.counter_culture_fix_counts
|
1722
|
+
|
1723
|
+
expect(product2.reload.special_poly_images_count).to eq(0)
|
1724
|
+
expect(employee.reload.special_poly_images_count).to eq(2)
|
1725
|
+
expect(product1.reload.special_poly_images_count).to eq(1)
|
1726
|
+
end
|
1727
|
+
|
1728
|
+
it "can deal with changes to condition" do
|
1729
|
+
img1 = PolyImage.create(imageable: employee)
|
1730
|
+
expect {img1.update_attributes!(url: special_url)}
|
1731
|
+
.to change { employee.reload.special_poly_images_count }.from(0).to(1)
|
1732
|
+
end
|
1733
|
+
|
1734
|
+
it "can deal with changes to condition" do
|
1735
|
+
img1 = PolyImage.create(imageable: employee, url: special_url)
|
1736
|
+
expect {img1.update_attributes!(url: "normal url")}
|
1737
|
+
.to change { employee.reload.special_poly_images_count }.from(1).to(0)
|
1738
|
+
end
|
1739
|
+
end
|
1740
|
+
end
|
1588
1741
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class PolyImage < ActiveRecord::Base
|
2
|
+
belongs_to :imageable, polymorphic: true
|
3
|
+
counter_culture :imageable
|
4
|
+
counter_culture :imageable, column_name: 'poly_images_count_dup'
|
5
|
+
counter_culture :imageable, column_name: ->(i){i.special? ? 'special_poly_images_count' : nil },
|
6
|
+
column_names: {
|
7
|
+
["poly_images.url LIKE ?", '%special%'] => 'special_poly_images_count',
|
8
|
+
}
|
9
|
+
|
10
|
+
|
11
|
+
def special?
|
12
|
+
url && url.include?('special')
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
data/spec/schema.rb
CHANGED
@@ -174,4 +174,23 @@ ActiveRecord::Schema.define(:version => 20120522160158) do
|
|
174
174
|
t.integer "person_id", :null => false
|
175
175
|
t.integer "monetary_value", :null => false
|
176
176
|
end
|
177
|
+
|
178
|
+
#polymorphic
|
179
|
+
create_table "poly_images", :force => true do |t|
|
180
|
+
t.integer "imageable_id", :null => true
|
181
|
+
t.string "imageable_type", :null => true
|
182
|
+
t.string "url"
|
183
|
+
end
|
184
|
+
create_table "poly_employees", :force => true do |t|
|
185
|
+
t.string "name"
|
186
|
+
t.integer "poly_images_count", :default => 0, :null => false
|
187
|
+
t.integer "poly_images_count_dup", :default => 0, :null => false
|
188
|
+
t.integer "special_poly_images_count", :default => 0, :null => false
|
189
|
+
end
|
190
|
+
create_table "poly_products", :primary_key => 'pp_pk_id', :force => true do |t|
|
191
|
+
t.string "brand_name"
|
192
|
+
t.integer "poly_images_count", :default => 0, :null => false
|
193
|
+
t.integer "poly_images_count_dup", :default => 0, :null => false
|
194
|
+
t.integer "special_poly_images_count", :default => 0, :null => false
|
195
|
+
end
|
177
196
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: counter_culture
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Magnus von Koeller
|
@@ -204,6 +204,9 @@ files:
|
|
204
204
|
- spec/models/has_string_id.rb
|
205
205
|
- spec/models/industry.rb
|
206
206
|
- spec/models/person.rb
|
207
|
+
- spec/models/poly_employee.rb
|
208
|
+
- spec/models/poly_image.rb
|
209
|
+
- spec/models/poly_product.rb
|
207
210
|
- spec/models/post.rb
|
208
211
|
- spec/models/post_comment.rb
|
209
212
|
- spec/models/product.rb
|