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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8651d71833facf2f685c6fcbd0ae922bb55e295d
4
- data.tar.gz: dab5d1dfecaa1a880171f5378de590385cf11218
3
+ metadata.gz: 84e93f1182982eccea8a717da0a95ebb3e4cfef8
4
+ data.tar.gz: 87e88e5de74ddf5c23d7df9ff3273ce3f0b17fdf
5
5
  SHA512:
6
- metadata.gz: 817f09ec93f18c4a638b1db6e34ac811a73abe2b96179bc471547cce713bc8029d7131e9577f9aac152ad82993db2e922f1a2f68d3d0541de6b7e944db05ce1c
7
- data.tar.gz: a7332118c2325d9124ad1053dddb6a947e2fc2e416bdb6999e7d5c36e3cb5452c8a8ba48a6ea21d0a13d364334990cf9ee2a840bfb00b7dd738683465fdb9ed4
6
+ metadata.gz: 0c7286393c38d78c7d59817dfe5dae1c7c05484d3f8c186f3c56b5f0dcaa8c0da3e922aad3395ea94c08cc638536633dfbdd8c3338edf9fd17927eea029ff024
7
+ data.tar.gz: 4872ec4223bc8a7ebdc0ca31852e0e7358121dc32f49c8302f744d451b5e32d3a17fcf972dfa8b9cd8be3c21753c2aea38cfec8b2e2fac86ae47cc5a4b1c4079
@@ -1,3 +1,8 @@
1
+ ## 1.5.0 (March 21, 2017)
2
+
3
+ New features:
4
+ - Support for counter caches on one-level polymorphic relationships
5
+
1
6
  ## 1.4.0 (March 21, 2017)
2
7
 
3
8
  Improvements:
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 currently does *not* support polymorphic associations. Check [this issue](https://github.com/magnusvk/counter_culture/issues/4) for progress and alternatives.
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.4.0
1
+ 1.5.0
@@ -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.4.0 ruby lib
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.4.0"
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
- klass.where(relation_primary_key(relation) => id_to_change).update_all updates.join(', ')
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
- value = klass.where("#{klass.table_name}.#{relation_primary_key(first)} = ?", foreign_key_value).first if foreign_key_value
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
- klass = reflect.klass
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
- def relation_klass(relation)
150
- relation_reflect(relation).klass
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
- def relation_primary_key(relation)
166
- relation_reflect(relation).association_primary_key
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 Rails.version >= "5.1.0"
115
- foreign_key_changed = saved_changes[counter.first_level_relation_foreign_key].present?
116
- delta_column_changed = (counter.delta_column && saved_changes[counter.delta_column].present?)
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
- # if we're provided a custom set of column names with conditions, use them; just use the
30
- # column name otherwise
31
- # which class does this relation ultimately point to? that's where we have to start
29
+ associated_model_classes.each do |associated_model_class|
30
+ Reconciliation.new(counter, changes, options, associated_model_class).perform
31
+ end
32
32
 
33
- scope = relation_class
33
+ @reconciled = true
34
+ end
34
35
 
35
- # respect the deleted_at column if it exists
36
- scope = scope.where("#{model.table_name}.deleted_at IS NULL") if model.column_names.include?('deleted_at')
36
+ private
37
37
 
38
- counter_column_names = column_names || {nil => counter_cache_name}
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
- # iterate over all the possible counter cache column names
41
- counter_column_names.each do |where, column_name|
42
- # if the column name is nil, that means those records don't affect
43
- # counts; we don't need to do anything in that case. but we allow
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
- # select join column and count (from above) as well as cache column ('column_name') for later comparison
49
- counts_query = scope.select("#{relation_class.table_name}.#{relation_class.primary_key}, #{relation_class.table_name}.#{relation_reflect(relation).association_primary_key}, #{count_select} AS count, #{relation_class.table_name}.#{column_name}")
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
- # iterate in batches; otherwise we might run out of memory when there's a lot of
61
- # instances and we try to load all their counts at once
62
- batch_size = options.fetch(:batch_size, CounterCulture.config.batch_size)
56
+ class Reconciliation
57
+ attr_reader :counter, :options, :relation_class
63
58
 
64
- counts_query.group(full_primary_key(relation_class)).find_in_batches(batch_size: batch_size) do |records|
65
- # now iterate over all the models and see whether their counts are right
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
- track_change(record, column_name, count)
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
- # use update_all because it's faster and because a fixed counter-cache shouldn't update the timestamp
74
- relation_class.where(relation_class.primary_key => record.send(relation_class.primary_key)).update_all(column_name => count)
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
- @reconciled = true
81
- end
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
- private
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
- # keep track of what we fixed, e.g. for a notification email
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
- def count_select
97
- # if a delta column is provided use SUM, otherwise use COUNT
98
- return @count_select if @count_select
99
- if delta_column
100
- @count_select = "SUM(COALESCE(#{self_table_name}.#{delta_column},0))"
101
- else
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
- def relation_class
107
- @relation_class ||= counter.relation_klass(counter.relation)
108
- end
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
- def self_table_name
111
- @self_table_name ||= if relation_class.table_name == model.table_name
112
- "#{model.table_name}_#{model.table_name}"
113
- else
114
- model.table_name
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
- def join_clauses
119
- return @join_clauses if defined?(@join_clauses)
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
- # we need to work our way back from the end-point of the relation to this class itself;
122
- # make a list of arrays pointing to the second-to-last, third-to-last, etc.
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
- # store joins in an array so that we can later apply column-specific conditions
126
- @join_clauses = reverse_relation.map do |cur_relation|
127
- reflect = relation_reflect(cur_relation)
128
- if relation_class.table_name == reflect.active_record.table_name
129
- join_table_name = "#{relation_class.table_name}_#{relation_class.table_name}"
130
- else
131
- join_table_name = reflect.active_record.table_name
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,3 @@
1
+ class PolyEmployee < ActiveRecord::Base
2
+ has_many :poly_images, as: :imageable
3
+ 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
@@ -0,0 +1,4 @@
1
+ class PolyProduct < ActiveRecord::Base
2
+ self.primary_key = :pp_pk_id
3
+ has_many :poly_images, as: :imageable
4
+ end
@@ -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.0
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