platanus 0.0.29 → 0.0.30

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.
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Platanus
2
- VERSION = "0.0.29" # 0.1 will come with tests!
2
+ VERSION = "0.0.30" # 0.1 will come with tests!
3
3
  end
data/platanus.gemspec CHANGED
@@ -14,4 +14,6 @@ Gem::Specification.new do |gem|
14
14
  gem.name = "platanus"
15
15
  gem.require_paths = ["lib","lib/platanus"]
16
16
  gem.version = Platanus::VERSION
17
+
18
+ gem.add_runtime_dependency "multi_json", [">= 1.3.2"]
17
19
  end
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.29
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-29 00:00:00.000000000 Z
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