|
| 1 | +require 'active_support' |
| 2 | +require 'active_record' |
| 3 | + |
| 4 | +module HasMagicColumns #:nodoc: |
| 5 | + module ActiveRecord |
| 6 | + module ClassMethods |
| 7 | + def has_magic_columns(options = {}) |
| 8 | + unless magical? |
| 9 | + # Associations |
| 10 | + has_many :magic_attribute_relationships, :as => :owner, :dependent => :destroy |
| 11 | + has_many :magic_attributes, :through => :magic_attribute_relationships, :dependent => :destroy |
| 12 | + |
| 13 | + # Eager loading - EXPERIMENTAL! |
| 14 | + if options[:eager] |
| 15 | + class_eval do |
| 16 | + def after_initialize |
| 17 | + initialize_magic_columns |
| 18 | + end |
| 19 | + end |
| 20 | + end |
| 21 | + |
| 22 | + # Inheritence |
| 23 | + cattr_accessor :inherited_from |
| 24 | + |
| 25 | + # if options[:through] is supplied, treat as an inherited relationship |
| 26 | + if self.inherited_from = options[:through] |
| 27 | + class_eval do |
| 28 | + def inherited_magic_columns |
| 29 | + raise "Cannot inherit MagicColumns from a non-existant association: #{@inherited_from}" unless self.class.method_defined?(inherited_from)# and self.send(inherited_from) |
| 30 | + self.send(inherited_from).magic_columns |
| 31 | + end |
| 32 | + end |
| 33 | + alias_method :magic_columns, :inherited_magic_columns unless method_defined? :magic_columns |
| 34 | + |
| 35 | + # otherwise the calling model has the relationships |
| 36 | + else |
| 37 | + has_many :magic_column_relationships, :as => :owner, :dependent => :destroy |
| 38 | + has_many :magic_columns, :through => :magic_column_relationships, :dependent => :destroy |
| 39 | + end |
| 40 | + |
| 41 | + # Hook into Base |
| 42 | + class_eval do |
| 43 | + alias_method :reload_without_magic, :reload |
| 44 | + alias_method :create_or_update_without_magic, :create_or_update |
| 45 | + alias_method :read_attribute_without_magic, :read_attribute |
| 46 | + end |
| 47 | + end |
| 48 | + include InstanceMethods |
| 49 | + |
| 50 | + # Add Magic to Base |
| 51 | + alias_method :reload, :reload_with_magic |
| 52 | + alias_method :read_attribute, :read_attribute_with_magic |
| 53 | + alias_method :create_or_update, :create_or_update_with_magic |
| 54 | + end |
| 55 | + |
| 56 | + def magical? |
| 57 | + self.included_modules.include?(InstanceMethods) |
| 58 | + end |
| 59 | + end |
| 60 | + |
| 61 | + module InstanceMethods #:nodoc: |
| 62 | + # Reinitialize MagicColumns and MagicAttributes when Model is reloaded |
| 63 | + def reload_with_magic |
| 64 | + initialize_magic_columns |
| 65 | + reload_without_magic |
| 66 | + end |
| 67 | + |
| 68 | + def update_attributes(new_attributes) |
| 69 | + attributes = new_attributes.stringify_keys |
| 70 | + magic_attrs = magic_columns.map(&:name) |
| 71 | + |
| 72 | + super(attributes.select{ |k, v| !magic_attrs.include?(k) }) |
| 73 | + attributes.select{ |k, v| magic_attrs.include?(k) }.each do |k, v| |
| 74 | + col = find_magic_column_by_name(k) |
| 75 | + attr = find_magic_attribute_by_column(col).first |
| 76 | + attr.update_attributes(:value => v) |
| 77 | + end |
| 78 | + end |
| 79 | + |
| 80 | + private |
| 81 | + |
| 82 | + # Save MagicAttributes from @attributes |
| 83 | + def create_or_update_with_magic |
| 84 | + if result = create_or_update_without_magic |
| 85 | + magic_columns.each do |column| |
| 86 | + value = @attributes[column.name] |
| 87 | + existing = find_magic_attribute_by_column(column) |
| 88 | + |
| 89 | + unless column.datatype == 'check_box_multiple' |
| 90 | + (attr = existing.first) ? |
| 91 | + update_magic_attribute(attr, value) : |
| 92 | + create_magic_attribute(column, value) |
| 93 | + else |
| 94 | + #TODO - make this more efficient |
| 95 | + value = [value] unless value.is_a? Array |
| 96 | + existing.map(&:destroy) if existing |
| 97 | + value.collect {|v| create_magic_attribute(column, v)} |
| 98 | + end |
| 99 | + end |
| 100 | + end |
| 101 | + result |
| 102 | + end |
| 103 | + |
| 104 | + # Load (lazily) MagicAttributes or fall back |
| 105 | + def method_missing(method_id, *args) |
| 106 | + super(method_id, *args) |
| 107 | + rescue NoMethodError |
| 108 | + method_name = method_id.to_s |
| 109 | + attr_names = magic_columns.map(&:name) |
| 110 | + initialize_magic_columns and retry if attr_names.include?(method_name) or |
| 111 | + (md = /[\?|\=]/.match(method_name) and |
| 112 | + attr_names.include?(md.pre_match)) |
| 113 | + super(method_id, *args) |
| 114 | + end |
| 115 | + |
| 116 | + # Load the MagicAttribute(s) associated with attr_name and cast them to proper type. |
| 117 | + def read_attribute_with_magic(attr_name) |
| 118 | + return read_attribute_without_magic(attr_name) if column_for_attribute(attr_name) # filter for regular columns |
| 119 | + attr_name = attr_name.to_s |
| 120 | + |
| 121 | + if !(value = @attributes[attr_name]).nil? |
| 122 | + if column = find_magic_column_by_name(attr_name) |
| 123 | + if value.is_a? Array |
| 124 | + value.map {|v| column.type_cast(v)} |
| 125 | + else |
| 126 | + column.type_cast(value) |
| 127 | + end |
| 128 | + else |
| 129 | + value |
| 130 | + end |
| 131 | + else |
| 132 | + nil |
| 133 | + end |
| 134 | + end |
| 135 | + |
| 136 | + # Lookup all MagicAttributes and setup @attributes |
| 137 | + def initialize_magic_columns |
| 138 | + magic_columns.each do |column| |
| 139 | + attribute = find_magic_attribute_by_column(column) |
| 140 | + name = column.name |
| 141 | + |
| 142 | + # Validation |
| 143 | + self.class.validates_presence_of(name) if column.is_required? |
| 144 | + |
| 145 | + # Write attribute |
| 146 | + unless column.datatype == 'check_box_multiple' |
| 147 | + (attr = attribute.first) ? |
| 148 | + write_attribute(name, attr.to_s) : |
| 149 | + write_attribute(name, column.default) |
| 150 | + else |
| 151 | + write_attribute(name, attribute.map(&:to_s)) |
| 152 | + end |
| 153 | + end |
| 154 | + end |
| 155 | + |
| 156 | + def find_magic_attribute_by_column(column) |
| 157 | + magic_attributes.to_a.find_all {|attr| attr.magic_column_id == column.id} |
| 158 | + end |
| 159 | + |
| 160 | + def find_magic_column_by_name(attr_name) |
| 161 | + magic_columns.to_a.find {|column| column.name == attr_name} |
| 162 | + end |
| 163 | + |
| 164 | + def create_magic_attribute(magic_column, value) |
| 165 | + magic_attributes << MagicAttribute.create(:magic_column => magic_column, :value => value) |
| 166 | + end |
| 167 | + |
| 168 | + def update_magic_attribute(magic_attribute, value) |
| 169 | + magic_attribute.update_attributes(:value => value) |
| 170 | + end |
| 171 | + end |
| 172 | + |
| 173 | + # mix into Active Record |
| 174 | + ::ActiveRecord::Base.extend ClassMethods |
| 175 | + |
| 176 | + %w{ models }.each do |dir| |
| 177 | + path = File.join(File.dirname(__FILE__), '../app', dir) |
| 178 | + $LOAD_PATH << path |
| 179 | + ActiveSupport::Dependencies.autoload_paths << path |
| 180 | + ActiveSupport::Dependencies.autoload_once_paths.delete(path) |
| 181 | + end |
| 182 | + end |
| 183 | +end |
0 commit comments