active_tools 0.0.5 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/README.md +55 -0
- data/active_tools.gemspec +1 -0
- data/copy +2 -0
- data/lib/active_tools/action_pack/action_dispatch/flash_stack.rb +2 -0
- data/lib/active_tools/action_pack/action_view/perform_as_tree.rb +38 -0
- data/lib/active_tools/action_pack/action_view.rb +1 -0
- data/lib/active_tools/active_model/delegate_attributes.rb +6 -30
- data/lib/active_tools/active_model/valid_with/fake_errors.rb +12 -0
- data/lib/active_tools/active_model/valid_with.rb +53 -0
- data/lib/active_tools/active_record/adaptive_belongs_to/adapter.rb +195 -0
- data/lib/active_tools/active_record/adaptive_belongs_to.rb +120 -0
- data/lib/active_tools/active_record/custom_counter_cache/instance_methods.rb +51 -0
- data/lib/active_tools/active_record/custom_counter_cache.rb +64 -0
- data/lib/active_tools/active_record/record_id.rb +4 -4
- data/lib/active_tools/activemodel.rb +1 -0
- data/lib/active_tools/activerecord.rb +2 -0
- data/lib/active_tools/core_extension/method_digger.rb +42 -0
- data/lib/active_tools/core_extension.rb +1 -0
- data/lib/active_tools/version.rb +1 -1
- data/spec/active_tools/active_model/delegate_attributes_spec.rb +49 -0
- data/spec/spec_helper.rb +7 -0
- metadata +33 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6884fd00e3b38429d219a9d533116fa46c824de0
|
4
|
+
data.tar.gz: 5fa48a6d56370b0e68729b71ac24977f6c327bfd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 478603a760745f50193cbe24358e39fefef37792a766b0d31bad36f686b127812deb76f80e9d8d11cdc963a20d3d773287350855d1af7a8db1cb93c44fddf342
|
7
|
+
data.tar.gz: c8f2fbae68a570892e9f212a0bc9c1c611867d064e0212eb8668fe4ed3515aa096482dae36dd95ea30ef872a59a5a8dccb10a6bb4767676c0fabfdbc6d7281ce
|
data/.gitignore
CHANGED
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
delegate_attributes
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby-2.0.0-p247
|
data/README.md
CHANGED
@@ -1,3 +1,58 @@
|
|
1
|
+
# DOCS ARE UNDER CONSTRUCTION
|
2
|
+
|
3
|
+
### For now, the most usable feature of active tools is 'custom counters' - my implementation of <t>counter through</t> solution
|
4
|
+
|
5
|
+
#### Look here! Typical data structure
|
6
|
+
|
7
|
+
Country
|
8
|
+
|
9
|
+
class Country # has 'products_count' column
|
10
|
+
...
|
11
|
+
end
|
12
|
+
|
13
|
+
Made in ...
|
14
|
+
|
15
|
+
class MadeIn # has 'products_count' column
|
16
|
+
belongs_to :country
|
17
|
+
|
18
|
+
custom_counter_cache_for :country => {:made_ins_count => 1, :products_count => :products_count}
|
19
|
+
|
20
|
+
# So, when MadeIn created/deleted, Country's 'made_ins_count' incremented/decremented by 1 and 'products_count' by MadeIn's 'products_count' value
|
21
|
+
...
|
22
|
+
end
|
23
|
+
|
24
|
+
Category (has parent)
|
25
|
+
|
26
|
+
class Category # has 'products_count' column
|
27
|
+
acts_as_nested_set # has parent and children (!)
|
28
|
+
|
29
|
+
custom_counter_cache_for "parent*" => {:children_count => 1, :products_count => :products_count}
|
30
|
+
|
31
|
+
# So, when Category created/deleted, parent's 'children_count' incremented/decremented by 1 and 'products_count' by Category's 'products_count' value
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
Brand name
|
36
|
+
|
37
|
+
class Brand # has 'products_count' column
|
38
|
+
...
|
39
|
+
end
|
40
|
+
|
41
|
+
Product itself
|
42
|
+
|
43
|
+
class Product < ActiveRecord::Base
|
44
|
+
belongs_to :category
|
45
|
+
belongs_to :brand
|
46
|
+
belongs_to :made_in
|
47
|
+
|
48
|
+
custom_counter_cache_for :made_in => {:products_count => 1, :country => {:products_count => 1}}, :category => {:products_count => 1, "parent*" => {:products_count => 1}}, :brand => {:products_count => 1}
|
49
|
+
|
50
|
+
# You can use nested options... it is very very useful :)
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
Thanks
|
55
|
+
|
1
56
|
# ActiveTools
|
2
57
|
|
3
58
|
TODO: Write a gem description
|
data/active_tools.gemspec
CHANGED
@@ -15,6 +15,7 @@ Gem::Specification.new do |gem|
|
|
15
15
|
gem.rubyforge_project = "active_tools"
|
16
16
|
|
17
17
|
gem.add_dependency "rails"
|
18
|
+
gem.add_development_dependency "rspec"
|
18
19
|
|
19
20
|
gem.files = `git ls-files`.split($/)
|
20
21
|
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
data/copy
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
module ActiveTools
|
2
|
+
module ActionPack
|
3
|
+
module ActionView
|
4
|
+
module PerformAsTree
|
5
|
+
def self.line(item, children_method, depth = 0, &block)
|
6
|
+
yield item, depth
|
7
|
+
item.send(children_method).each do |child|
|
8
|
+
line(child, children_method, depth+1, &block)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module OnLoadActionView
|
16
|
+
|
17
|
+
def perform_as_tree(scope, options = {}, &block)
|
18
|
+
options = options.with_indifferent_access
|
19
|
+
children_method = options[:children_method]||:children
|
20
|
+
parent_method = options[:parent_method]||:parent
|
21
|
+
id_key = options[:id]||nil
|
22
|
+
scope = case scope
|
23
|
+
when ::ActiveRecord::Relation then
|
24
|
+
parent_key = scope.klass.reflections[children_method].foreign_key
|
25
|
+
scope.where(parent_key => id_key)
|
26
|
+
when ::Array, ::Set then
|
27
|
+
scope.select {|item| item.send(parent_method) == id_key}
|
28
|
+
else
|
29
|
+
raise(TypeError, "ActiveRecord::Relation, Array or Set expected, #{scope.class.name} passed!")
|
30
|
+
end
|
31
|
+
scope.each do |item|
|
32
|
+
ActionPack::ActionView::PerformAsTree.line(item, children_method, 0, &block)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
@@ -3,57 +3,33 @@ module ActiveTools
|
|
3
3
|
module DelegateAttributes
|
4
4
|
extend ::ActiveSupport::Concern
|
5
5
|
|
6
|
-
included do
|
7
|
-
class FakeErrors < ::ActiveModel::Errors
|
8
|
-
private
|
9
|
-
def normalize_message(attribute, message, options)
|
10
|
-
message ||= :invalid
|
11
|
-
end
|
12
|
-
end
|
13
|
-
|
14
|
-
end
|
15
|
-
|
16
6
|
module ClassMethods
|
17
7
|
def delegate_attributes(*args)
|
18
8
|
options = args.extract_options!
|
19
9
|
errors_option = options.delete(:errors)
|
20
10
|
writer_option = options.delete(:writer)
|
11
|
+
prefix_option = options.delete(:prefix)
|
21
12
|
|
22
13
|
writer_regexp = /=\z/
|
23
14
|
readers = args.select {|a| a.to_s !=~ writer_regexp}
|
24
15
|
writers = args.select {|a| a.to_s =~ writer_regexp}
|
25
16
|
if writer_option == true
|
26
|
-
writers
|
17
|
+
writers |= readers.map {|a| "#{a}="}
|
27
18
|
end
|
28
19
|
|
29
20
|
class_eval do
|
30
21
|
delegate *(readers + writers), options.dup
|
22
|
+
unless errors_option == false
|
23
|
+
valid_with options[:to], :attributes => Hash[readers.map {|a| [a, a]}], :fit => errors_option.to_s == "fit", :prefix => prefix_option
|
24
|
+
end
|
31
25
|
end
|
32
|
-
|
33
|
-
unless errors_option == false
|
34
|
-
class_eval <<-EOV
|
35
|
-
validate do
|
36
|
-
object = #{options[:to]}
|
37
|
-
#{"object.instance_variable_set(:@errors, FakeErrors.new(object))" if errors_option.to_s == "fit"}
|
38
|
-
if !object.valid?
|
39
|
-
object.errors.messages.each do |attribute, suberrors|
|
40
|
-
if attribute.to_s.in? %w{#{readers.join(" ")}}
|
41
|
-
suberrors.each do |suberror|
|
42
|
-
errors.add(attribute, suberror)
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
48
|
-
EOV
|
49
|
-
end
|
50
26
|
end
|
51
27
|
end
|
52
28
|
|
53
29
|
|
54
30
|
end
|
55
31
|
|
56
|
-
|
32
|
+
::ActiveModel::Validations.send(:include, ActiveModel::DelegateAttributes)
|
57
33
|
|
58
34
|
end
|
59
35
|
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'active_tools/active_model/valid_with/fake_errors'
|
2
|
+
module ActiveTools
|
3
|
+
module ActiveModel
|
4
|
+
module ValidWith
|
5
|
+
extend ::ActiveSupport::Concern
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def valid_with(*args)
|
9
|
+
options = args.extract_options!
|
10
|
+
object_name = args.first
|
11
|
+
passed_attr_map = options.delete(:attributes)
|
12
|
+
prefix = options.delete(:prefix)
|
13
|
+
raise(TypeError, "Option :attributes must be a Hash. #{passed_attr_map.class} passed!") unless passed_attr_map.is_a?(Hash)
|
14
|
+
attr_map_name = :"_valid_with_#{object_name}"
|
15
|
+
unless respond_to?(attr_map_name)
|
16
|
+
class_attribute attr_map_name
|
17
|
+
self.send("#{attr_map_name}=", passed_attr_map.with_indifferent_access)
|
18
|
+
else
|
19
|
+
self.send(attr_map_name).merge!(passed_attr_map)
|
20
|
+
end
|
21
|
+
|
22
|
+
if passed_attr_map.any?
|
23
|
+
class_eval <<-EOV
|
24
|
+
validate do
|
25
|
+
if object = #{object_name}
|
26
|
+
#{"object.instance_variable_set(:@errors, ActiveTools::ActiveModel::ValidWith::FakeErrors.new(object))" if options[:fit] == true}
|
27
|
+
if !object.valid?
|
28
|
+
object.errors.messages.each do |attribute, suberrors|
|
29
|
+
if local_attribute = self.#{attr_map_name}[attribute]
|
30
|
+
suberrors.each do |suberror|
|
31
|
+
errors.add(["#{prefix}", local_attribute].select(&:present?).join("_"), suberror)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
EOV
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
::ActiveModel::Validations.send(:include, ActiveModel::ValidWith)
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
module OnLoadActiveRecord
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
@@ -0,0 +1,195 @@
|
|
1
|
+
module ActiveTools
|
2
|
+
module ActiveRecord
|
3
|
+
module AdaptiveBelongsTo
|
4
|
+
class Adapter
|
5
|
+
attr_reader :association, :options
|
6
|
+
|
7
|
+
delegate :target, :target_id, :klass, :owner, :reflection, :to => :association
|
8
|
+
|
9
|
+
def initialize(association, options = {})
|
10
|
+
@association = association
|
11
|
+
@options = options.with_indifferent_access
|
12
|
+
@foreign_key = reflection.foreign_key
|
13
|
+
@remote_attributes = @options[:remote_attributes]
|
14
|
+
@init_proc = @options[:init_proc]
|
15
|
+
@update_if = @options[:update_if]
|
16
|
+
@destroy_if = @options[:destroy_if]
|
17
|
+
@uniq_by = Array(@options[:uniq_by]).map(&:to_s)
|
18
|
+
@association.load_target
|
19
|
+
end
|
20
|
+
|
21
|
+
def read(name)
|
22
|
+
valid_attribute?(name)
|
23
|
+
target.send(name) if target
|
24
|
+
end
|
25
|
+
|
26
|
+
def write(name, value)
|
27
|
+
valid_attribute?(name)
|
28
|
+
if value != read(name)
|
29
|
+
store_backup!
|
30
|
+
create_template!
|
31
|
+
target.send("#{name}=", value)
|
32
|
+
if same_as_backup?
|
33
|
+
restore_backup!
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def try_commit
|
39
|
+
try_commit_existed || try_update
|
40
|
+
end
|
41
|
+
|
42
|
+
def try_destroy
|
43
|
+
try_destroy_backup
|
44
|
+
try_destroy_target
|
45
|
+
end
|
46
|
+
|
47
|
+
def try_update
|
48
|
+
if updateable_backup?
|
49
|
+
begin
|
50
|
+
@backup.update(attributes(@template, *@remote_attributes))
|
51
|
+
rescue ::ActiveRecord::StaleObjectError
|
52
|
+
@backup.reload
|
53
|
+
try_update
|
54
|
+
end
|
55
|
+
self.target = @backup
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def try_commit_existed
|
60
|
+
if @template.present? && @uniq_by.any? && existed = detect_existed
|
61
|
+
self.target = existed
|
62
|
+
try_destroy_updateable_backup
|
63
|
+
true
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def try_destroy_backup
|
68
|
+
if destroyable_backup?
|
69
|
+
begin
|
70
|
+
@backup.destroy
|
71
|
+
rescue ::ActiveRecord::StaleObjectError
|
72
|
+
@backup.reload
|
73
|
+
try_destroy_backup
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def try_destroy_updateable_backup
|
79
|
+
if updateable_backup?
|
80
|
+
begin
|
81
|
+
@backup.destroy
|
82
|
+
rescue ::ActiveRecord::StaleObjectError
|
83
|
+
@backup.reload
|
84
|
+
try_destroy_updateable_backup
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def try_destroy_target(force = false)
|
90
|
+
if destroyable_target?
|
91
|
+
begin
|
92
|
+
target.destroy
|
93
|
+
rescue ::ActiveRecord::StaleObjectError
|
94
|
+
target.reload
|
95
|
+
try_destroy_target
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def clear!
|
101
|
+
@template = nil
|
102
|
+
@backup = nil
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def detect_existed
|
108
|
+
outer_values = {}
|
109
|
+
where_values = {}
|
110
|
+
@uniq_by.each do |attribute|
|
111
|
+
relation_options_call = "#{attribute}_relation_options"
|
112
|
+
if klass.respond_to?(relation_options_call)
|
113
|
+
values = @template.send(relation_options_call)
|
114
|
+
outer_values.merge!(values[:outer_values])
|
115
|
+
where_values.merge!(values[:where_values])
|
116
|
+
else
|
117
|
+
where_values[attribute] = @template.send(attribute)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
klass.includes(outer_values).where(where_values).limit(1).first
|
121
|
+
end
|
122
|
+
|
123
|
+
def updateable_backup?
|
124
|
+
@backup.present? && @update_if.try(:call, @backup)
|
125
|
+
end
|
126
|
+
|
127
|
+
def destroyable_backup?
|
128
|
+
@backup.present? && !@backup.destroyed? && @destroy_if.try(:call, @backup)
|
129
|
+
end
|
130
|
+
|
131
|
+
def destroyable_target?
|
132
|
+
target.try(:persisted?) && !target.destroyed? && @destroy_if.try(:call, target)
|
133
|
+
end
|
134
|
+
|
135
|
+
def attributes(object, *attrs)
|
136
|
+
Hash[attrs.map {|a| [a, object.send(a)]}]
|
137
|
+
end
|
138
|
+
|
139
|
+
def create_template!
|
140
|
+
if target.nil? || @template.nil?
|
141
|
+
self.target = template
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def restore_backup!
|
146
|
+
if @backup
|
147
|
+
self.target = @backup
|
148
|
+
@backup = nil
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def store_backup!
|
153
|
+
if target.try(:persisted?)
|
154
|
+
@backup ||= target
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def same_as_backup?
|
159
|
+
@backup.present? && eval(@remote_attributes.map {|a| "@backup.send(:#{a}) == target.send(:#{a})"}.join(" && "))
|
160
|
+
end
|
161
|
+
|
162
|
+
def valid_attribute?(name)
|
163
|
+
raise(NameError, "Undefined remote attribute :#{name}!") unless @remote_attributes.include?(name.to_s)
|
164
|
+
end
|
165
|
+
|
166
|
+
def target=(record)
|
167
|
+
if owner.persisted?
|
168
|
+
association.send(:replace_keys, record)
|
169
|
+
association.set_inverse_instance(record)
|
170
|
+
association.instance_variable_set(:@updated, true) if record != @backup
|
171
|
+
association.target = record
|
172
|
+
else
|
173
|
+
association.replace(record)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def template
|
178
|
+
@template ||=
|
179
|
+
if target.try(:persisted?)
|
180
|
+
klass.new(attributes(target, *@remote_attributes))
|
181
|
+
elsif target.nil?
|
182
|
+
klass.new
|
183
|
+
elsif target.try(:new_record?)
|
184
|
+
target.dup
|
185
|
+
end
|
186
|
+
@template.tap do |t|
|
187
|
+
@init_proc.try(:call, t)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'active_tools/active_record/adaptive_belongs_to/adapter'
|
2
|
+
module ActiveTools
|
3
|
+
module ActiveRecord
|
4
|
+
module AdaptiveBelongsTo
|
5
|
+
extend ::ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
|
12
|
+
def relation_options_under(*args)
|
13
|
+
path = args.extract_options!
|
14
|
+
local_attribute = args.first
|
15
|
+
local_method = "#{local_attribute}_relation_options"
|
16
|
+
|
17
|
+
define_singleton_method local_method do |instance = nil|
|
18
|
+
outer_values = {}
|
19
|
+
where_values = {}
|
20
|
+
path.each do |assoc_name, remote_attributes|
|
21
|
+
reflection = reflections[assoc_name]
|
22
|
+
target = instance.try(reflection.name)
|
23
|
+
outer_values[reflection.name] = {}
|
24
|
+
Array(remote_attributes).each do |remote_attribute|
|
25
|
+
remote_method = "#{remote_attribute}_relation_options"
|
26
|
+
if reflection.klass.respond_to?(remote_method)
|
27
|
+
deeper = reflection.klass.send(remote_method, target)
|
28
|
+
outer_values[reflection.name].merge!(deeper[:outer_values])
|
29
|
+
where_values.merge!(deeper[:where_values])
|
30
|
+
else
|
31
|
+
where_values[reflection.table_name] ||= {}.with_indifferent_access
|
32
|
+
where_values[reflection.table_name][remote_attribute] = target.try(remote_attribute)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
{:outer_values => outer_values, :where_values => where_values}
|
37
|
+
end
|
38
|
+
|
39
|
+
class_eval do
|
40
|
+
define_method local_method do
|
41
|
+
self.class.send(local_method, self)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
def adaptive_belongs_to(*args)
|
48
|
+
options = args.extract_options!
|
49
|
+
assoc_name = args.first
|
50
|
+
unless reflection = reflections[assoc_name]
|
51
|
+
raise(ArgumentError, ":#{assoc_name} method doesn't look like an association accessor!")
|
52
|
+
end
|
53
|
+
adapter_name = "#{assoc_name}_"
|
54
|
+
config_name = "#{assoc_name}_adapter_options"
|
55
|
+
|
56
|
+
raise(TypeError, "Option :attributes must be a Hash. #{options[:attributes].class} passed!") unless options[:attributes].is_a?(Hash)
|
57
|
+
attr_map = options.delete(:attributes).with_indifferent_access
|
58
|
+
|
59
|
+
valid_with assoc_name, :attributes => attr_map
|
60
|
+
|
61
|
+
class_attribute config_name
|
62
|
+
self.send("#{config_name}=", options.merge(:remote_attributes => attr_map.keys))
|
63
|
+
|
64
|
+
class_eval <<-EOV
|
65
|
+
after_validation do
|
66
|
+
#{adapter_name}.try_commit
|
67
|
+
end
|
68
|
+
|
69
|
+
after_commit do
|
70
|
+
#{adapter_name}.try_destroy_backup
|
71
|
+
#{adapter_name}.clear!
|
72
|
+
end
|
73
|
+
|
74
|
+
after_destroy do
|
75
|
+
#{adapter_name}.try_destroy
|
76
|
+
end
|
77
|
+
|
78
|
+
def #{adapter_name}
|
79
|
+
@#{adapter_name} ||= ActiveTools::ActiveRecord::AdaptiveBelongsTo::Adapter.new(association(:#{assoc_name}), #{config_name})
|
80
|
+
end
|
81
|
+
EOV
|
82
|
+
|
83
|
+
attr_map.each do |remote_attribute, local_attribute|
|
84
|
+
relation_options_under(local_attribute, assoc_name => remote_attribute)
|
85
|
+
class_eval do
|
86
|
+
define_method local_attribute do
|
87
|
+
send(adapter_name).read(remote_attribute)
|
88
|
+
end
|
89
|
+
|
90
|
+
define_method "#{local_attribute}=" do |value|
|
91
|
+
send(adapter_name).write(remote_attribute, value)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# attr_map.each do |remote_attribute, local_attribute|
|
97
|
+
# class_eval <<-EOV
|
98
|
+
# def #{local_attribute}_adaptive
|
99
|
+
# [{:#{assoc_name} => {}}, {:#{reflections[assoc_name].table_name} => {:#{remote_attribute} => #{local_attribute}}}]
|
100
|
+
# end
|
101
|
+
#
|
102
|
+
# def #{local_attribute}
|
103
|
+
# #{adapter_name}.read(:#{remote_attribute})
|
104
|
+
# end
|
105
|
+
#
|
106
|
+
# def #{local_attribute}=(value)
|
107
|
+
# #{adapter_name}.write(:#{remote_attribute}, value)
|
108
|
+
# end
|
109
|
+
# EOV
|
110
|
+
# end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
module OnLoadActiveRecord
|
117
|
+
include ActiveRecord::AdaptiveBelongsTo
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module ActiveTools
|
2
|
+
module ActiveRecord
|
3
|
+
module CustomCounterCache
|
4
|
+
module InstanceMethods
|
5
|
+
def custom_counter_cache_after_create(assoc_name, reflection, assoc_mapping)
|
6
|
+
if record = send(assoc_name)
|
7
|
+
ActiveRecord::CustomCounterCache.digger(self, record, assoc_mapping) do |parent, cache_column, value|
|
8
|
+
parent.class.update_counters(parent.id, cache_column => value)
|
9
|
+
end
|
10
|
+
@_after_create_custom_counter_called = true
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def custom_counter_cache_before_destroy(assoc_name, reflection, assoc_mapping)
|
15
|
+
foreign_key = reflection.foreign_key.to_sym
|
16
|
+
unless destroyed_by_association && destroyed_by_association.foreign_key.to_sym == foreign_key
|
17
|
+
record = send(assoc_name)
|
18
|
+
if record && !self.destroyed?
|
19
|
+
ActiveRecord::CustomCounterCache.digger(self, record, assoc_mapping) do |parent, cache_column, value|
|
20
|
+
parent.class.update_counters(parent.id, cache_column => -value)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def custom_counter_cache_after_update(assoc_name, reflection, assoc_mapping)
|
27
|
+
foreign_key = reflection.foreign_key
|
28
|
+
if (@_after_create_custom_counter_called ||= false)
|
29
|
+
@_after_create_custom_counter_called = false
|
30
|
+
elsif send(:attribute_changed?, foreign_key) && !new_record? && (Rails.version >= "4.1.0" ? association(assoc_name).constructable? : defined?(reflection.klass.to_s.camelize))
|
31
|
+
model = reflection.klass
|
32
|
+
foreign_key_was = attribute_was foreign_key
|
33
|
+
foreign_key = attribute foreign_key
|
34
|
+
|
35
|
+
if foreign_key && model.respond_to?(:increment_counter) && to_increment = model.find_by_id(foreign_key)
|
36
|
+
ActiveRecord::CustomCounterCache.digger(self, to_increment, assoc_mapping) do |parent, cache_column, value|
|
37
|
+
parent.class.update_counters(parent.id, cache_column => value)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
if foreign_key_was && model.respond_to?(:decrement_counter) && to_decrement = model.find_by_id(foreign_key_was)
|
41
|
+
ActiveRecord::CustomCounterCache.digger(self, to_decrement, assoc_mapping) do |parent, cache_column, value|
|
42
|
+
parent.class.update_counters(parent.id, cache_column => -value)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'active_tools/active_record/custom_counter_cache/instance_methods'
|
2
|
+
|
3
|
+
module ActiveTools
|
4
|
+
module ActiveRecord
|
5
|
+
module CustomCounterCache
|
6
|
+
extend ::ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def custom_counter_cache_for(*args)
|
13
|
+
mapping = args.extract_options!
|
14
|
+
mapping.each do |assoc_name, value|
|
15
|
+
assoc_name = assoc_name.to_s
|
16
|
+
if assoc_name.last == "*"
|
17
|
+
if value.is_a?(Hash)
|
18
|
+
assoc_mapping = value.merge(assoc_name => value)
|
19
|
+
end
|
20
|
+
assoc_name = assoc_name[0..-2]
|
21
|
+
else
|
22
|
+
assoc_mapping = value
|
23
|
+
end
|
24
|
+
reflection = reflections[assoc_name.to_sym]
|
25
|
+
|
26
|
+
unless method_defined? :custom_counter_cache_after_create
|
27
|
+
include ActiveRecord::CustomCounterCache::InstanceMethods
|
28
|
+
end
|
29
|
+
|
30
|
+
after_create lambda { |record|
|
31
|
+
record.custom_counter_cache_after_create(assoc_name, reflection, assoc_mapping)
|
32
|
+
}
|
33
|
+
|
34
|
+
before_destroy lambda { |record|
|
35
|
+
record.custom_counter_cache_before_destroy(assoc_name, reflection, assoc_mapping)
|
36
|
+
}
|
37
|
+
|
38
|
+
after_update lambda { |record|
|
39
|
+
record.custom_counter_cache_after_update(assoc_name, reflection, assoc_mapping)
|
40
|
+
}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.digger(owner, object, mapping)
|
46
|
+
object.method_digger(mapping) do |object, key, response, value|
|
47
|
+
if response && !response.is_a?(::ActiveRecord::Base)
|
48
|
+
count = case value
|
49
|
+
when String, Symbol then owner.send(value)
|
50
|
+
when Fixnum then value
|
51
|
+
end
|
52
|
+
yield object, key, count
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
module OnLoadActiveRecord
|
61
|
+
include ActiveRecord::CustomCounterCache
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
@@ -1,5 +1,5 @@
|
|
1
1
|
module ActiveTools
|
2
|
-
module
|
2
|
+
module ActiveRecord
|
3
3
|
module RecordId
|
4
4
|
extend ::ActiveSupport::Concern
|
5
5
|
|
@@ -10,18 +10,18 @@ module ActiveTools
|
|
10
10
|
end
|
11
11
|
|
12
12
|
def record_id
|
13
|
-
"#{self.class.model_name.singular}_#{try(
|
13
|
+
"#{self.class.model_name.singular}_#{try(self.class.primary_key)||uniq_id}"
|
14
14
|
end
|
15
15
|
|
16
16
|
def uniq_id
|
17
|
-
Base64.urlsafe_encode64(Time.now._dump)
|
17
|
+
Base64.urlsafe_encode64(Time.now.send(:_dump))
|
18
18
|
end
|
19
19
|
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
23
23
|
module OnLoadActiveRecord
|
24
|
-
include
|
24
|
+
include ActiveRecord::RecordId
|
25
25
|
end
|
26
26
|
|
27
27
|
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module ActiveTools
|
2
|
+
module CoreExtension
|
3
|
+
|
4
|
+
module MethodDigger
|
5
|
+
module ObjectExtension
|
6
|
+
def method_digger(tree, &block)
|
7
|
+
tree.stringify_keys!
|
8
|
+
tree.each do |method, value|
|
9
|
+
if method.last == "*"
|
10
|
+
method = method[0..-2]
|
11
|
+
cycle_call(method) do |nested|
|
12
|
+
yield self, method, nested, value
|
13
|
+
if value.is_a?(Hash) && !nested.nil?
|
14
|
+
nested.method_digger(value, &block)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
else
|
18
|
+
response = try(:send, method)
|
19
|
+
yield self, method, response, value
|
20
|
+
if value.is_a?(Hash) && !response.nil?
|
21
|
+
response.method_digger(value, &block)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def cycle_call(method, &block)
|
28
|
+
object = self
|
29
|
+
export = []
|
30
|
+
while object = object.try(:send, method)
|
31
|
+
yield object if block_given?
|
32
|
+
export << object
|
33
|
+
end
|
34
|
+
export
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
::Object.send(:include, ObjectExtension)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -3,6 +3,7 @@ require 'active_tools/core_extension/deep_merge'
|
|
3
3
|
require 'active_tools/core_extension/hashup'
|
4
4
|
require 'active_tools/core_extension/merge_hashup'
|
5
5
|
require 'active_tools/core_extension/kabuki'
|
6
|
+
require 'active_tools/core_extension/method_digger'
|
6
7
|
|
7
8
|
module ActiveTools
|
8
9
|
module CoreExtension
|
data/lib/active_tools/version.rb
CHANGED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ActiveTools::ActiveModel::DelegateAttributes do
|
4
|
+
class Parent
|
5
|
+
include ActiveModel::Validations
|
6
|
+
include ActiveTools::ActiveModel::DelegateAttributes
|
7
|
+
|
8
|
+
attr_accessor :child
|
9
|
+
|
10
|
+
delegate_attributes :name, to: :child, writer: true
|
11
|
+
delegate_attributes :name, to: :child, prefix: :prefixed, writer: true
|
12
|
+
end
|
13
|
+
|
14
|
+
class Child
|
15
|
+
include ActiveModel::Validations
|
16
|
+
include ActiveTools::ActiveModel::DelegateAttributes
|
17
|
+
|
18
|
+
attr_accessor :name
|
19
|
+
|
20
|
+
validates_presence_of :name
|
21
|
+
end
|
22
|
+
|
23
|
+
let(:teh_object) { Parent.new.tap{|parent| parent.child = teh_child} }
|
24
|
+
let(:teh_child) { Child.new }
|
25
|
+
|
26
|
+
it "delegates the given attribute from parent to child" do
|
27
|
+
teh_object.name = "Foo"
|
28
|
+
|
29
|
+
expect(teh_child.name).to eq("Foo")
|
30
|
+
expect(teh_object.name).to eq("Foo")
|
31
|
+
end
|
32
|
+
|
33
|
+
it "delegates the given attribute with a prefix from parent to child" do
|
34
|
+
teh_object.prefixed_name = "Bar"
|
35
|
+
|
36
|
+
expect(teh_child.name).to eq("Bar")
|
37
|
+
expect(teh_object.prefixed_name).to eq("Bar")
|
38
|
+
end
|
39
|
+
|
40
|
+
it "forwards the errors from child to parent" do
|
41
|
+
expect(teh_object.valid?).to be_false
|
42
|
+
expect(teh_object.errors.messages[:name]).to eq(["can't be blank"])
|
43
|
+
end
|
44
|
+
|
45
|
+
it "forwards the errors from child to parent via prefix" do
|
46
|
+
expect(teh_object.valid?).to be_false
|
47
|
+
expect(teh_object.errors.messages[:prefixed_name]).to eq(["can't be blank"])
|
48
|
+
end
|
49
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_tools
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Valery Kvon
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2013-
|
11
|
+
date: 2013-11-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -24,6 +24,20 @@ dependencies:
|
|
24
24
|
- - '>='
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
27
41
|
description: Missing tools for Rails developers
|
28
42
|
email:
|
29
43
|
- addagger@gmail.com
|
@@ -32,20 +46,30 @@ extensions: []
|
|
32
46
|
extra_rdoc_files: []
|
33
47
|
files:
|
34
48
|
- .gitignore
|
49
|
+
- .ruby-gemset
|
50
|
+
- .ruby-version
|
35
51
|
- Gemfile
|
36
52
|
- LICENSE.txt
|
37
53
|
- README.md
|
38
54
|
- Rakefile
|
39
55
|
- active_tools.gemspec
|
56
|
+
- copy
|
40
57
|
- lib/active_tools.rb
|
41
58
|
- lib/active_tools/action_pack/action_controller.rb
|
42
59
|
- lib/active_tools/action_pack/action_controller/path_helper.rb
|
43
60
|
- lib/active_tools/action_pack/action_dispatch.rb
|
44
61
|
- lib/active_tools/action_pack/action_dispatch/flash_stack.rb
|
45
62
|
- lib/active_tools/action_pack/action_view.rb
|
63
|
+
- lib/active_tools/action_pack/action_view/perform_as_tree.rb
|
46
64
|
- lib/active_tools/action_pack/action_view/tag_attributes.rb
|
47
65
|
- lib/active_tools/actionpack.rb
|
48
66
|
- lib/active_tools/active_model/delegate_attributes.rb
|
67
|
+
- lib/active_tools/active_model/valid_with.rb
|
68
|
+
- lib/active_tools/active_model/valid_with/fake_errors.rb
|
69
|
+
- lib/active_tools/active_record/adaptive_belongs_to.rb
|
70
|
+
- lib/active_tools/active_record/adaptive_belongs_to/adapter.rb
|
71
|
+
- lib/active_tools/active_record/custom_counter_cache.rb
|
72
|
+
- lib/active_tools/active_record/custom_counter_cache/instance_methods.rb
|
49
73
|
- lib/active_tools/active_record/record_id.rb
|
50
74
|
- lib/active_tools/activemodel.rb
|
51
75
|
- lib/active_tools/activerecord.rb
|
@@ -60,12 +84,15 @@ files:
|
|
60
84
|
- lib/active_tools/core_extension/kabuki/dump.rb
|
61
85
|
- lib/active_tools/core_extension/kabuki/zip.rb
|
62
86
|
- lib/active_tools/core_extension/merge_hashup.rb
|
87
|
+
- lib/active_tools/core_extension/method_digger.rb
|
63
88
|
- lib/active_tools/engine.rb
|
64
89
|
- lib/active_tools/misc.rb
|
65
90
|
- lib/active_tools/misc/input_source.rb
|
66
91
|
- lib/active_tools/misc/script_flow.rb
|
67
92
|
- lib/active_tools/railtie.rb
|
68
93
|
- lib/active_tools/version.rb
|
94
|
+
- spec/active_tools/active_model/delegate_attributes_spec.rb
|
95
|
+
- spec/spec_helper.rb
|
69
96
|
homepage: http://vkvon.ru/projects/active_tools
|
70
97
|
licenses: []
|
71
98
|
metadata: {}
|
@@ -85,9 +112,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
85
112
|
version: '0'
|
86
113
|
requirements: []
|
87
114
|
rubyforge_project: active_tools
|
88
|
-
rubygems_version: 2.0.
|
115
|
+
rubygems_version: 2.0.4
|
89
116
|
signing_key:
|
90
117
|
specification_version: 4
|
91
118
|
summary: ActionDispatch, ActionController, ActiveModel, ActiveRecord, ActiveSupport,
|
92
119
|
ActionView and core extensions
|
93
|
-
test_files:
|
120
|
+
test_files:
|
121
|
+
- spec/active_tools/active_model/delegate_attributes_spec.rb
|
122
|
+
- spec/spec_helper.rb
|