platanus 0.0.29 → 0.0.30
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/platanus/stacked2.rb +174 -0
- data/lib/platanus/version.rb +1 -1
- data/platanus.gemspec +2 -0
- metadata +20 -3
@@ -0,0 +1,174 @@
|
|
1
|
+
# stacked.rb : Stackable attributes for ActiveRecord
|
2
|
+
#
|
3
|
+
# Copyright April 2012, Ignacio Baixas +mailto:ignacio@platan.us+.
|
4
|
+
|
5
|
+
module Platanus
|
6
|
+
|
7
|
+
## Adds the has_stacked association to an ActiveRecord model.
|
8
|
+
#
|
9
|
+
# TODO
|
10
|
+
#
|
11
|
+
module StackedAttr2
|
12
|
+
|
13
|
+
class NotSupportedError < StandardError; end
|
14
|
+
|
15
|
+
def self.included(base)
|
16
|
+
base.extend ClassMethods
|
17
|
+
end
|
18
|
+
|
19
|
+
module ClassMethods
|
20
|
+
|
21
|
+
# Adds an stacked attribute to the model.
|
22
|
+
def has_stacked(_name, _options={})
|
23
|
+
|
24
|
+
# check option support
|
25
|
+
raise NotSupportedError.new('Only autosave mode is supported') if _options[:autosave] == false
|
26
|
+
raise NotSupportedError.new('has_many_through is not supported yet') if _options.has_key? :through
|
27
|
+
|
28
|
+
# prepare names
|
29
|
+
tname = _name.to_s
|
30
|
+
tname_single = tname.singularize
|
31
|
+
tname_class = _options.fetch(:class_name, tname_single.camelize)
|
32
|
+
|
33
|
+
# generate top_value property
|
34
|
+
top_value_prop = "top_#{tname_single}"
|
35
|
+
if _options.has_key? :top_value_key
|
36
|
+
belongs_to top_value_prop.to_sym, class_name: tname_class, foreign_key: _options.delete(:top_value_key)
|
37
|
+
elsif self.column_names.include? "#{top_value_prop}_id"
|
38
|
+
belongs_to top_value_prop.to_sym, class_name: tname_class
|
39
|
+
else
|
40
|
+
instance_var = "@_last_#{tname_single}".to_sym
|
41
|
+
send :define_method, top_value_prop do
|
42
|
+
# Storing the last stacked value will not prevent race conditions
|
43
|
+
# when simultaneous updates occur.
|
44
|
+
last = instance_variable_get instance_var
|
45
|
+
return last unless last.nil?
|
46
|
+
instance_variable_set(instance_var, self.send(tname).first)
|
47
|
+
end
|
48
|
+
send :define_method, "#{top_value_prop}=" do |_top|
|
49
|
+
instance_variable_set(instance_var, _top)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
send :private, "#{top_value_prop}="
|
53
|
+
|
54
|
+
prefix = if _options[:cache_prf].nil? then 'last_' else _options.delete(:cache_prf) end # TODO: deprecate
|
55
|
+
|
56
|
+
# generate virtual attributes
|
57
|
+
changes = {}
|
58
|
+
stacked_model = tname_class.constantize
|
59
|
+
stacked_model.accessible_attributes.each do |attr_name|
|
60
|
+
send :define_method, "#{attr_name}=" do |value| changes[attr_name] = value end
|
61
|
+
send :define_method, "#{attr_name}" do
|
62
|
+
return changes[attr_name] if changes.has_key? attr_name
|
63
|
+
return self.send(prefix + attr_name) if self.respond_to? prefix + attr_name # Return cached value if avaliable
|
64
|
+
top = self.send top_value_prop
|
65
|
+
return nil if top.nil?
|
66
|
+
return top.send attr_name
|
67
|
+
end
|
68
|
+
attr_accessible attr_name
|
69
|
+
end
|
70
|
+
|
71
|
+
# prepare cached attributes
|
72
|
+
to_cache = _options.delete(:cached)
|
73
|
+
unless to_cache.nil?
|
74
|
+
to_cache = to_cache.map do |cache_attr|
|
75
|
+
unless cache_attr.is_a? Hash
|
76
|
+
name = cache_attr.to_s
|
77
|
+
# attr_protected(prefix + name)
|
78
|
+
{ to: prefix + name, from: name }
|
79
|
+
else
|
80
|
+
# TODO: Test whether options are valid.
|
81
|
+
cache_attr
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# callbacks
|
87
|
+
on_stack = _options.delete(:on_stack)
|
88
|
+
|
89
|
+
# limits and ordering
|
90
|
+
# TODO: Support other kind of ordering, this would require to reevaluate top on every push
|
91
|
+
_options[:order] = 'created_at DESC, id DESC'
|
92
|
+
_options[:limit] = 10 if _options[:limit].nil?
|
93
|
+
|
94
|
+
# setup main association
|
95
|
+
has_many _name, _options
|
96
|
+
|
97
|
+
cache_step = ->(_ctx, _top, _top_is_new) {
|
98
|
+
# cache required fields
|
99
|
+
return if to_cache.nil?
|
100
|
+
to_cache.each do |cache_attr|
|
101
|
+
value = if cache_attr.has_key? :from
|
102
|
+
_top.nil? ? _top : _top.send(cache_attr[:from])
|
103
|
+
else
|
104
|
+
_ctx.send(cache_attr[:virtual], _top, _top_is_new)
|
105
|
+
end
|
106
|
+
_ctx.send(cache_attr[:to].to_s + '=', value)
|
107
|
+
end
|
108
|
+
}
|
109
|
+
|
110
|
+
after_step = ->(_ctx, _top) {
|
111
|
+
# update top value property
|
112
|
+
_ctx.send("#{top_value_prop}=", _top)
|
113
|
+
|
114
|
+
# execute after callback
|
115
|
+
_ctx.send(on_stack, _top) unless on_stack.nil?
|
116
|
+
}
|
117
|
+
|
118
|
+
# before saving model, load changes from virtual attributes.
|
119
|
+
before_save do
|
120
|
+
if changes.count > 0
|
121
|
+
obj = stacked_model.new(changes)
|
122
|
+
changes = {} # since push sometimes saves, then we must prevent a stack overflow by unsetting changes here.
|
123
|
+
|
124
|
+
cache_step.call(self, obj, true)
|
125
|
+
self.save! if self.new_record? # make sure there is an id BEFORE pushing
|
126
|
+
raise ActiveRecord::RecordInvalid.new(obj) unless send(tname).send('<<', obj)
|
127
|
+
after_step.call(self, obj)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
send :define_method, "push_#{tname_single}!" do |obj|
|
132
|
+
self.class.transaction do
|
133
|
+
|
134
|
+
# cache, then save if new, then push and finally process state
|
135
|
+
cache_step.call(self, obj, true)
|
136
|
+
self.save! if self.new_record? # make sure there is an id BEFORE pushing
|
137
|
+
raise ActiveRecord::RecordInvalid.new(obj) unless send(tname).send('<<', obj)
|
138
|
+
after_step.call(self, obj)
|
139
|
+
|
140
|
+
self.save! if self.changed? # Must save again, no other way...
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
send :define_method, "push_#{tname_single}" do |obj|
|
145
|
+
begin
|
146
|
+
return send("push_#{tname_single}!", obj)
|
147
|
+
rescue ActiveRecord::RecordInvalid
|
148
|
+
return false
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
send :define_method, "restore_#{tname}!" do
|
153
|
+
self.class.transaction do
|
154
|
+
|
155
|
+
# find current top, then restore stack state
|
156
|
+
top = self.send(_name).first
|
157
|
+
cache_step.call(self, top, false)
|
158
|
+
after_step.call(self, top)
|
159
|
+
|
160
|
+
self.save! if self.changed?
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
send :define_method, "restore_#{tname}" do
|
165
|
+
begin
|
166
|
+
return send("restore_#{tname}!")
|
167
|
+
rescue ActiveRecord::RecordInvalid
|
168
|
+
return false
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
data/lib/platanus/version.rb
CHANGED
data/platanus.gemspec
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: platanus
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.30
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,8 +9,24 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-10-
|
13
|
-
dependencies:
|
12
|
+
date: 2012-10-30 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: multi_json
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.3.2
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 1.3.2
|
14
30
|
description: Platan.us utility gem
|
15
31
|
email:
|
16
32
|
- ignacio@platan.us
|
@@ -36,6 +52,7 @@ files:
|
|
36
52
|
- lib/platanus/onetime.rb
|
37
53
|
- lib/platanus/serializers/json_sym.rb
|
38
54
|
- lib/platanus/stacked.rb
|
55
|
+
- lib/platanus/stacked2.rb
|
39
56
|
- lib/platanus/templates/prawn.rb
|
40
57
|
- lib/platanus/templates/spreadsheet.rb
|
41
58
|
- lib/platanus/traceable.rb
|