Module: Platanus::StackedAttr::ClassMethods
- Defined in:
- lib/platanus/stacked.rb
Instance Method Summary collapse
-
#has_stacked(_name, _options = {}) ⇒ Object
Adds an stacked attribute to the model.
Instance Method Details
#has_stacked(_name, _options = {}) ⇒ Object
Adds an stacked attribute to the model.
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 |
# File 'lib/platanus/stacked.rb', line 22 def has_stacked(_name, ={}) # check option support raise NotSupportedError.new('Only autosave mode is supported') if [:autosave] == false raise NotSupportedError.new('has_many_through is not supported yet') if .has_key? :through # prepare names tname = _name.to_s tname_single = tname.singularize tname_class = .fetch(:class_name, tname_single.camelize) # generate top_value property top_value_prop = "top_#{tname_single}" if .has_key? :top_value_key belongs_to top_value_prop.to_sym, class_name: tname_class, foreign_key: .delete(:top_value_key) elsif self.column_names.include? "#{top_value_prop}_id" belongs_to top_value_prop.to_sym, class_name: tname_class else instance_var = "@_last_#{tname_single}".to_sym send :define_method, top_value_prop do # Storing the last stacked value will not prevent race conditions # when simultaneous updates occur. last = instance_variable_get instance_var return last unless last.nil? instance_variable_set(instance_var, self.send(tname).first) end send :define_method, "#{top_value_prop}=" do |_top| instance_variable_set(instance_var, _top) end end send :private, "#{top_value_prop}=" # prepare cached attributes to_cache = .delete(:cached) to_cache_prf = if [:cache_prf].nil? then 'last_' else .delete(:cache_prf) end # TODO: deprecate unless to_cache.nil? to_cache = to_cache.map do |cache_attr| unless cache_attr.is_a? Hash name = cache_attr.to_s # attr_protected(to_cache_prf + name) send :define_method, name do self.send(to_cache_prf + name) end # generate read-only aliases without prefix. TODO: deprecate { to: to_cache_prf + name, from: name } else # TODO: Test whether options are valid. cache_attr end end end # callbacks on_stack = .delete(:on_stack) # limits and ordering # TODO: Support other kind of ordering, this would require to reevaluate top on every push [:order] = 'created_at DESC, id DESC' [:limit] = 10 if [:limit].nil? # setup main association has_many _name, cache_step = ->(_ctx, _top, _top_is_new) { # cache required fields return if to_cache.nil? to_cache.each do |cache_attr| value = if cache_attr.has_key? :from _top.nil? ? _top : _top.send(cache_attr[:from]) else _ctx.send(cache_attr[:virtual], _top, _top_is_new) end _ctx.send(cache_attr[:to].to_s + '=', value) end } after_step = ->(_ctx, _top) { # update top value property _ctx.send("#{top_value_prop}=", _top) # execute after callback _ctx.send(on_stack, _top) unless on_stack.nil? } send :define_method, "push_#{tname_single}!" do |obj| self.class.transaction do # cache, then save if new, then push and finally process state cache_step.call(self, obj, true) self.save! if self.new_record? # make sure there is an id BEFORE pushing raise ActiveRecord::RecordInvalid.new(obj) unless send(tname).send('<<',obj) after_step.call(self, obj) self.save! if self.changed? # Must save again, no other way... end end send :define_method, "push_#{tname_single}" do |obj| begin return send("push_#{tname_single}!", obj) rescue ActiveRecord::RecordInvalid return false end end send :define_method, "restore_#{tname}!" do self.class.transaction do # find current top, then restore stack state top = self.send(_name).first cache_step.call(self, top, false) after_step.call(self, top) self.save! if self.changed? end end send :define_method, "restore_#{tname}" do begin return send("restore_#{tname}!") rescue ActiveRecord::RecordInvalid return false end end end |