active_tools 0.0.5 → 0.0.6
Sign up to get free protection for your applications and to get access to all the features.
- 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
|