datts_right 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,98 @@
1
+ require 'datts_right/instance_methods'
2
+ require 'datts_right/datt'
3
+ #require 'datts_right/query_methods'
4
+ #require 'datts_right/exceptions'
5
+
6
+ module DattsRight
7
+ def has_datts(options={})
8
+ include DattsRight::InstanceMethods
9
+ #include DattsRight::QueryMethods
10
+ #ActiveRecord::QueryMethods.extend DattsRight::QueryMethods
11
+ #include DattsRight::Exceptions
12
+
13
+ cattr_accessor :datts_options
14
+ self.datts_options = options
15
+
16
+ has_many :datts, :as => :attributable, :dependent => :destroy
17
+
18
+ # Carry out delayed actions before save
19
+ before_save :build_dynamic_attributes
20
+
21
+ # scope :scope_self when looking through attributes so we don't look through all datts
22
+ # Why? What if you have Friend and Page models.
23
+ # * Some Phone records have a datt :price
24
+ # * Some Page records have a datt :price
25
+ #
26
+ # When we do Page.find_by_price(400) we want to search only the datts that belong to Page
27
+ # and we want to disregard the rest of the datts.
28
+ scope :scope_self, lambda { joins(:datts).where("datts.attributable_type = :klass", :klass => self.name) }
29
+ scope :dattr_key, lambda { |attr_key| scope_self.joins(:datts).where("datts.attr_key = :attr_key", :attr_key => attr_key)}
30
+ scope :dattr_type, lambda { |object_type| scope_self.joins(:datts).where("object_type = :object_type", :object_type => object_type) }
31
+
32
+ scope :order_datt, lambda { |attr_key_with_order, object_type|
33
+ # possible attr_key_with_order forms: "field_name", "field_name ASC", "field_name DESC"
34
+ split_attr_key_with_order = attr_key_with_order.split(" ")
35
+ attr_key = split_attr_key_with_order.first
36
+ order_by = split_attr_key_with_order.last if split_attr_key_with_order.size > 1
37
+ order_value = "datts.#{object_type}_value"
38
+ order_value << " #{order_by}" if order_by
39
+ scope_self.dattr_key(attr_key).joins(:datts).dattr_type(object_type).order(order_value)
40
+ }
41
+
42
+ scope :where_datt, lambda { |opts|
43
+ # TODO accept stuff other than the normal hash
44
+ # Lifted from AR::Relation#build_where
45
+ attributes = case opts
46
+ when String, Array
47
+ self.expand_hash_conditions_for_aggregates(opts)
48
+ when Hash
49
+ opts
50
+ end
51
+ results = self#joins(:datts)
52
+ # Look for all DattsRight records whose:
53
+ # 1. Has all dynamic_attributes
54
+ # 2. Those dynamic attributes match there conditions
55
+ attributes.each do |k, v|
56
+ conditions = "exists (" +
57
+ "select 1 from datts datt where " +
58
+ # restricting the attributable_id to pages.id forces all the exists clauses to reference a single attributable record. Layman's terms, assuming Page uses datts_right:
59
+ # find only pages that has the matching dynamic attributes
60
+ "#{self.table_name}.id = datt.attributable_id " +
61
+ "and datt.attributable_type = :attributable_type " +
62
+ "and datt.name = :name and datt.#{Datt.attr_column(v)} = :value" +
63
+ ")"
64
+ results = results.where(conditions, :attributable_type => self.name, :name => k.to_s, :value => v)
65
+ end
66
+ results
67
+ }
68
+
69
+ def self.method_missing(method_id, *arguments)
70
+ # TODO better way to hook this into the rails code, and not define my own
71
+ if method_id.to_s =~ /^find_(all_|last_)?by_([_a-zA-Z]\w*)$/
72
+ attributes = $2.split("_and_")
73
+ #puts "Will find by #{attributes.inspect}"
74
+ arg = arguments.first
75
+ scope_self.joins(:datts).where("datts.#{Datt.attr_column(arg)} = :value", :value => arg)
76
+ end
77
+ end
78
+
79
+ # Override AR::Base#respond_to? so we can return the matchers even if the
80
+ # attribute doesn't exist in the actual columns. Is this expensive?
81
+ def respond_to?(method_id, include_private=false)
82
+ # TODO perhaps we could save a cache somewhere of all methods used by
83
+ # any of the records of this class. that way, we can make this method
84
+ # act a bit more like AR::Base#respond_to?
85
+ # Ex:
86
+ # return true if all_attributes_exists?(match.attribute_names) || all_dynamic_attributes_exists?(match.attribute_names)
87
+ if match = ActiveRecord::DynamicFinderMatch.match(method_id)
88
+ return true
89
+ elsif match = ActiveRecord::DynamicScopeMatch.match(method_id)
90
+ return true
91
+ end
92
+
93
+ super
94
+ end
95
+ end
96
+ end
97
+
98
+ ActiveRecord::Base.extend DattsRight
@@ -0,0 +1,40 @@
1
+ class Datt < ActiveRecord::Base
2
+ # TODO
3
+ # Set table name from configuration
4
+ belongs_to :attributable, :polymorphic => true
5
+
6
+ def value
7
+ send("#{object_type}_value")
8
+ end
9
+
10
+ def value=(v)
11
+ object_type = self.class.attr_type?(v)
12
+
13
+ # only allow object_type changes if this is NOT a new record
14
+ # and it's text to string or vice versa
15
+ interchangeable_types = %w(text string)
16
+ #puts "It's currently a #{self.object_type}, and we wanna make it a #{object_type}"
17
+ if !new_record? && interchangeable_types.include?(self.object_type) && interchangeable_types.include?(object_type) && self.object_type != object_type
18
+ #puts "It's not new, and we're setting it to #{object_type}"
19
+ self.object_type = object_type
20
+ end
21
+ send("#{self.class.attr_column(v)}=", v)
22
+ end
23
+
24
+ def self.attr_type?(v)
25
+ object_type = if v.is_a?(Integer)
26
+ "integer"
27
+ elsif v.is_a?(Float)
28
+ "float"
29
+ elsif v.is_a?(TrueClass) || v.is_a?(FalseClass)
30
+ "boolean"
31
+ elsif v.is_a?(String)
32
+ v.size < 255 ? "string" : "text"
33
+ end
34
+ end
35
+
36
+ # Returns what column this kind of data should be in
37
+ def self.attr_column(v)
38
+ "#{self.attr_type?(v)}_value"
39
+ end
40
+ end
@@ -0,0 +1,10 @@
1
+ module DattsRight
2
+ module Exceptions
3
+ class AttributeExistsError < StandardError
4
+ attr_reader :reason
5
+ def initialize(reason)
6
+ @reason = reason
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,214 @@
1
+ module DattsRight
2
+ module InstanceMethods
3
+ def add_dynamic_attribute(name, klass)
4
+ unless attributes.keys.include?(name.to_s)
5
+ name = name.to_s
6
+ datt = datts.find_by_attr_key(name.underscore)
7
+ unless datt
8
+ datt = datts.create :name => name, :attr_key => name.underscore, :object_type => klass
9
+ dynamic_columns(true)
10
+ end
11
+ #puts "Just added #{name} to #{self.class.name}##{id}, and here are the dynamic_attributes: #{dynamic_columns}"
12
+ datt
13
+ else
14
+ false
15
+ end
16
+ end
17
+
18
+ def remove_dynamic_attribute(name)
19
+ name = name.to_s
20
+ datt = datts.find_by_attr_key(name)
21
+ if datt
22
+ datt.destroy
23
+ dynamic_columns(true)
24
+ end
25
+ end
26
+
27
+ # Works like ActiveRecord's attributes except it returns dynamic attributes only
28
+ def dynamic_attributes(reload=false)
29
+ @dynamic_attributes ||= {}
30
+ return @dynamic_attributes if !reload
31
+ #puts "Reloading dynamic_attributes"
32
+ datts.each do |datt|
33
+ #puts "Adding this to @dynamic_attributes: #{datt.attr_key} => #{datt.value}"
34
+ @dynamic_attributes.merge({datt.attr_key => datt.value})
35
+ end
36
+ #puts "Here are teh datts: #{datts.inspect}, and this is what we're returning: #{@dynamic_attributes}"
37
+ @dynamic_attributes.symbolize_keys!
38
+ end
39
+
40
+ # Because we cannot determine the difference between the inexistence
41
+ # between key value pair point to nil (eg :hi => nil) and
42
+ # the key not existing, we need to have a different method to return
43
+ # information about the record in this format:
44
+ # {:price => {:object_type => "integer", :value => 200}}
45
+ def dynamic_columns(reload=false)
46
+ @dynamic_columns ||= {}
47
+ @dynamic_columns if !reload
48
+ #puts "Building the dynamic_columns cache, and we'll look through #{datts.count} datts"
49
+ @dynamic_columns={}
50
+ datts.reload
51
+ datts.each do |datt|
52
+ #puts "Going through datt##{datt.id}"
53
+ dynamic_column = {
54
+ datt.attr_key.to_sym => {
55
+ :object_type => datt.object_type,
56
+ :value => datt.value
57
+ }
58
+ }
59
+ #puts "Added #{dynamic_column} to the dynamic columns"
60
+ @dynamic_columns.merge!(dynamic_column)
61
+ end
62
+ #puts "in dynamic_columns, returning: #{@dynamic_columns.inspect}"
63
+ @dynamic_columns
64
+ end
65
+
66
+ # Determines if the given attribute is a dynamic attribute.
67
+ def dynamic_attribute?(attr)
68
+ #puts "in dynamic_attribute?(:#{attr}). datts: #{datts.inspect}. Datt: #{Datt.all.inspect}"
69
+ #dynamic_columns.include?(attr.to_s)
70
+ !dynamic_columns[attr.to_sym].nil?
71
+ #return dynamic_attributes.include?(attr.to_sym) unless dynamic_attributes.empty?
72
+ #return false if self.class.column_names.include?(attr.to_s)
73
+ #false
74
+ end
75
+
76
+ # This overrides the attributes= defined in ActiveRecord::Base
77
+ # The only difference is that this doesn't check to see if the
78
+ # model responds_to the method before sending it
79
+ #def attributes=(new_attributes, guard_protected_attributes = true)
80
+ #return unless new_attributes.is_a?(Hash)
81
+ #attributes = new_attributes.stringify_keys
82
+
83
+ #multi_parameter_attributes = []
84
+ #attributes = sanitize_for_mass_assignment(attributes) if guard_protected_attributes
85
+
86
+ #attributes.each do |k, v|
87
+ #if k.include?("(")
88
+ #multi_parameter_attributes << [ k, v ]
89
+ #else
90
+ #send("#{k}=", v)
91
+ #end
92
+ #end
93
+
94
+ #assign_multiparameter_attributes(multi_parameter_attributes)
95
+ #end
96
+
97
+ # Overrides AR::Persistence#update_attributes
98
+ # Needed to change it because the AR::Persistence method updated the attributes
99
+ # method directly.
100
+ # Ex.
101
+ # self.attributes = attributes
102
+ #
103
+ # That won't work because our dynamic attributes aren't in the attributes field
104
+ def update_attributes(attributes)
105
+ # The following transaction covers any possible database side-effects of the
106
+ # attributes assignment. For example, setting the IDs of a child collection.
107
+ with_transaction_returning_status do
108
+ attributes.each do |k, v|
109
+ self.send("#{k}=", v)
110
+ end
111
+ save
112
+ end
113
+ end
114
+
115
+ # Overrides AR::Persistence#update_attributes!
116
+ # See DattsRight#update_attributes for the reason why
117
+ def update_attributes!(attributes)
118
+ with_transaction_returning_status do
119
+ attributes.each do |k, v|
120
+ self.send("#{k}=", v)
121
+ end
122
+ save!
123
+ end
124
+ end
125
+
126
+ private
127
+
128
+ # Called after validation on update so that dynamic attributes behave
129
+ # like normal attributes in the fact that the database is not touched
130
+ # until save is called.
131
+ def build_dynamic_attributes
132
+ return if @save_dynamic_attr.nil?
133
+ # This originally saves things into the "datts" column.
134
+ #write_attribute_without_dynamic_attributes "datts", @save_dynamic_attr
135
+ # We save things into the datts table
136
+ @save_dynamic_attr.each do |k, v|
137
+ k = k.to_s
138
+ datt = datts.find_by_attr_key(k)
139
+ #puts "to create or to update #{k}?"
140
+ if datt
141
+ datt.update_attribute(:value, v)
142
+ #puts "updated: #{datt.inspect}"
143
+ #else
144
+ #datt = datts.create! :name => k, :attr_key => k, :value => v
145
+ #puts "created: #{datt.inspect} among these datts: #{datts.inspect}"
146
+ end
147
+ end
148
+ @save_dynamic_attr = {}
149
+ true
150
+ end
151
+
152
+ # Implements dynamic-attributes as if real getter/setter methods
153
+ # were defined.
154
+ def method_missing(method_id, *args, &block)
155
+ #puts "in method_missing_without_dynamic_attributes. Checking #{method_id}"
156
+ begin
157
+ super method_id, *args, &block
158
+ rescue NoMethodError => e
159
+ attr_name = method_id.to_s.sub(/\=$/, '')
160
+ #puts "Now we check to see if price is a dynamic attribute. dynamic_columns: #{dynamic_columns}. is #{attr_name} in those dynamic_columns? #{dynamic_attribute?(attr_name)}"
161
+ if dynamic_attribute?(attr_name)
162
+ #puts "== And #{method_id} is a dynamic method"
163
+ if method_id.to_s =~ /\=$/
164
+ return write_attribute(attr_name, args[0])
165
+ else
166
+ return read_attribute(attr_name)
167
+ end
168
+ end
169
+ raise e
170
+ end
171
+ end
172
+
173
+ # Overrides AR::Base#read_attribute
174
+ def read_attribute(attr_name)
175
+ attr_name = attr_name.to_s
176
+ if dynamic_attribute?(attr_name)
177
+ #puts "trying to read datt #{attr_name}"
178
+ # If value is in the @save_dynamic_attr cache, and it's not blank
179
+ if !@save_dynamic_attr.blank? and @save_dynamic_attr[attr_name]
180
+ #puts "datt #{attr_name} IS in cache, so let's return its value"
181
+ # Then we return the value
182
+ return @save_dynamic_attr[attr_name]
183
+ else # The value is not there, or it's blank
184
+ #puts "datt #{attr_name} is NOT in cache, so let's load it from the datt table"
185
+ #attrs = read_attribute_without_dynamic_attributes(dynamic_attributes_options[:column_name].to_s)
186
+ #puts "Here are ALL the datts: #{Datt.all.inspect} and my id is: #{id}"
187
+ #puts "Here are my the datts: #{datts.inspect} and my id is: #{id}. and the first datt's value is #{datts.first.value}"
188
+ datt = datts.find_by_attr_key(attr_name)
189
+ #puts "This is the dat I find: #{datt.inspect}"
190
+ return datt.try(:value)
191
+ #attrs = attrs.nil? ? nil : YAML.load(attrs).symbolize_keys! unless attrs.is_a? Hash
192
+ #return nil if attrs.blank?
193
+ #return attrs[attr_name.to_sym]
194
+ end
195
+ end
196
+
197
+ super(attr_name)
198
+ end
199
+
200
+ # Overrides AR::Base#write_attribute
201
+ def write_attribute(attr_name, value)
202
+ #puts "attempting to write: #{attr_name} = #{value}"
203
+ if dynamic_attribute?(attr_name)
204
+ #puts "#{attr_name} is a dynamic_attribute"
205
+ attr_name = attr_name.to_s
206
+
207
+ @save_dynamic_attr ||= {} # create the cache if needed
208
+ return @save_dynamic_attr[attr_name] = value
209
+ end
210
+
211
+ super(attr_name, value)
212
+ end
213
+ end
214
+ end
@@ -0,0 +1,3 @@
1
+ class Page < ActiveRecord::Base
2
+ has_datts
3
+ end
@@ -0,0 +1,15 @@
1
+ module DattsRight
2
+ module QueryMethods
3
+ puts "in DattsRight::QueryMethods"
4
+ def order(*args)
5
+ puts "In DattsRight::QueryMethods#order with these args: #{args}"
6
+ relation = clone
7
+ puts "= This is the relation: #{relation.inspect}"
8
+ relation.order_values += args.flatten unless args.blank?
9
+ puts "= This is the relation.order_values: #{relation.order_values.inspect}"
10
+ relation
11
+ end
12
+ end
13
+ end
14
+
15
+ ActiveRecord::QueryMethods.extend DattsRight::QueryMethods
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Explain the generator
3
+
4
+ Example:
5
+ rails generate migration Thing
6
+
7
+ This will create:
8
+ what/will/it/create
@@ -0,0 +1,3 @@
1
+ class MigrationGenerator < Rails::Generators::NamedBase
2
+ source_root File.expand_path('../templates', __FILE__)
3
+ end
@@ -0,0 +1,20 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe Datt do
4
+ before do
5
+ reset_database
6
+ @page = Page.create
7
+ end
8
+
9
+ it "should allow transferring between String and Text" do
10
+ @page.add_dynamic_attribute(:foo, "string")
11
+ @page.foo = "this is a string"
12
+ @page.save
13
+ @page.foo = "t"*300
14
+ @page.save
15
+ @page.datts.find_by_attr_key("foo").object_type.should == "text"
16
+ @page.foo = "this is a string again"
17
+ @page.save
18
+ @page.datts.find_by_attr_key("foo").object_type.should == "string"
19
+ end
20
+ end
@@ -0,0 +1,299 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe DattsRight do
4
+ before do
5
+ reset_database
6
+ @page = Page.create
7
+ end
8
+
9
+ describe ".add_dynamic_attribute(attr_key, object_type)" do
10
+ it "should add a dynamic attribute with nil value" do
11
+ @page.add_dynamic_attribute(:rocks, "string")
12
+ @page.dynamic_attributes[:rocks].should be_nil
13
+ end
14
+
15
+ it "should ignore when trying to add same attribute" do
16
+ @page.add_dynamic_attribute(:rocks, "string")
17
+ @page.add_dynamic_attribute(:rocks, "integer")
18
+ @page.dynamic_columns[:rocks][:object_type].should == "string"
19
+ end
20
+
21
+ it "should return false if the method that is being added already exists" do
22
+ @page.add_dynamic_attribute(:name, "text").should be_false
23
+ end
24
+
25
+ it "should not make any changes to the original attribute" do
26
+ @page.update_attribute(:name, "juno")
27
+ @page.add_dynamic_attribute(:name, "text").should be_false
28
+ @page.name.should == "juno"
29
+ end
30
+ end
31
+
32
+ describe ".remove_dynamic_attribute(attr_key)" do
33
+ it "should remove the attribute completely" do
34
+ @page.add_dynamic_attribute(:rocks, "string")
35
+ @page.remove_dynamic_attribute(:rocks)
36
+ lambda { @page.rocks }.should raise_error(NoMethodError)
37
+ end
38
+
39
+ it "should not explode if the attribute being removed isn't there" do
40
+ @page.add_dynamic_attribute(:rocks, "string")
41
+ @page.remove_dynamic_attribute(:rocks)
42
+ lambda {
43
+ @page.remove_dynamic_attribute(:rocks)
44
+ }.should_not raise_error(NoMethodError)
45
+ end
46
+ end
47
+
48
+ describe ".dynamic_columns(reload)" do
49
+ it "should return an array of symbols of the available dynamic columns" do
50
+ @page.add_dynamic_attribute(:rocks, "string")
51
+ @page.is_dynamic_attribute?(:rocks).should be_true
52
+ end
53
+ end
54
+
55
+ it "should not allow arbitrary creation of attributes" do
56
+ lambda {
57
+ @page.some_field = "woot"
58
+ }.should raise_error(NoMethodError)
59
+ end
60
+
61
+ it "should allow saving of integers" do
62
+ @page.add_dynamic_attribute(:price, "integer")
63
+ @page.price = 300
64
+ @page.save
65
+ @page = Page.last
66
+ @page.price.should == 300
67
+ end
68
+
69
+ it "should allow saving of floats" do
70
+ @page.add_dynamic_attribute(:price, "float")
71
+ @page.price = 300.0
72
+ @page.save
73
+ @page = Page.last
74
+ @page.price.should == 300.0
75
+ end
76
+
77
+ it "should allow saving of true" do
78
+ @page.add_dynamic_attribute(:real, "boolean")
79
+ @page.real = true
80
+ @page.save
81
+ @page = Page.last
82
+ @page.real.should be_true
83
+ end
84
+
85
+ it "should allow saving of false" do
86
+ @page.add_dynamic_attribute(:real, "boolean")
87
+ @page.real = false
88
+ @page.save
89
+ @page = Page.last
90
+ @page.real.should be_false
91
+ end
92
+
93
+ it "should allow saving of strings" do
94
+ @page.add_dynamic_attribute(:real, "string")
95
+ @page.real = "its real alright"
96
+ @page.save
97
+ @page = Page.last
98
+ @page.real.should == "its real alright"
99
+ end
100
+
101
+ it "should allow saving of text" do
102
+ @page.add_dynamic_attribute(:real, "text")
103
+ @page.real = "t"*256
104
+ @page.save
105
+ @page = Page.last
106
+ @page.real.should == "t"*256
107
+ end
108
+
109
+ it "should cache the attribute" do
110
+ @page.add_dynamic_attribute(:price, "integer")
111
+ @page.price = 200
112
+ @page.price.should == 200
113
+ end
114
+
115
+ it "should not save the attribute if save on the attributable object isn't called" do
116
+ @page.add_dynamic_attribute(:price, "integer")
117
+ @page.price = 200
118
+ @page = Page.last
119
+ @page.price.should be_nil
120
+ end
121
+
122
+ it "should allow overwriting of values" do
123
+ @page.add_dynamic_attribute(:price, "integer")
124
+ @page.price = 200
125
+ @page.save
126
+ @page = Page.last
127
+ @page.price = 300
128
+ @page.save
129
+ @page = Page.last
130
+ @page.price.should == 300
131
+ end
132
+
133
+ it "should allow nullifying of attributes, but keeping the fields there" do
134
+ @page.add_dynamic_attribute(:farce, "string")
135
+ @page.farce = "Nothing here my friend"
136
+ @page.save
137
+ @page.farce = nil
138
+ @page.is_dynamic_attribute?(:farce).should be_true
139
+ end
140
+
141
+ describe "on dynamic find_by methods" do
142
+ it "should work" do
143
+ @page.add_dynamic_attribute(:price, "integer")
144
+ @page.price = 400
145
+ @page.save
146
+
147
+ @page_2 = Page.create
148
+ @page_2.add_dynamic_attribute(:price, "integer")
149
+ @page_2.price = 500
150
+ @page_2.save
151
+
152
+ Page.find_by_price(400).should include(@page)
153
+ Page.find_by_price(400).should_not include(@page_2)
154
+ end
155
+
156
+ it "should allow chaining" do
157
+ @page.add_dynamic_attribute(:price, "integer")
158
+ @page.name = "fixed"
159
+ @page.price = 400
160
+ @page.save
161
+
162
+ @page_2 = Page.create
163
+ @page_2.add_dynamic_attribute(:price, "integer")
164
+ @page_2.price = 500
165
+ @page_2.save
166
+
167
+ @pages = Page.where(:name => "fixed").find_by_price(400)
168
+ @pages.should include(@page)
169
+ @pages.should_not include(@page_2)
170
+ end
171
+ end
172
+
173
+ it "should allow multiple attributes" do
174
+ @page.add_dynamic_attribute(:price, "integer")
175
+ @page.price = 400
176
+ @page.add_dynamic_attribute(:farce, "string")
177
+ @page.farce = "hi"
178
+ @page.save
179
+
180
+ @page_2 = Page.create
181
+ @page_2.add_dynamic_attribute(:price, "integer")
182
+ @page_2.price = 400
183
+ @page_2.add_dynamic_attribute(:farce, "string")
184
+ @page_2.farce = "hitt"
185
+ @page_2.save
186
+
187
+ @pages = Page.find_by_farce_and_price("hi", 400)
188
+ @pages.should include(@page)
189
+ @pages.should_not include(@page_2)
190
+ end
191
+
192
+ describe "when the value being assigned is not of the same type (with exceptions)" do
193
+ before do
194
+ @page.add_dynamic_attribute(:price, "integer")
195
+ end
196
+
197
+ it "should not save the value" do
198
+ @page.price = 200
199
+ @page.save
200
+ @page.price = "hi there"
201
+ @page.save
202
+ Page.last.price.should == 200
203
+ end
204
+ end
205
+
206
+ it "should allow mass assignment" do
207
+ @page.add_dynamic_attribute(:price, "integer")
208
+ @page.add_dynamic_attribute(:farce, "string")
209
+ @page.update_attributes(:price => 200, :farce => "hi")
210
+ @page.price.should == 200
211
+ @page.farce.should == "hi"
212
+
213
+ @page.update_attributes!(:price => 300, :farce => "not farce!")
214
+ @page.price.should == 300
215
+ @page.farce.should == "not farce!"
216
+ end
217
+
218
+ it "should allow use of update_attribute" do
219
+ @page.add_dynamic_attribute(:price, "integer")
220
+ @page.update_attribute(:price, 200)
221
+ @page.price.should == 200
222
+ end
223
+
224
+ describe "ordering" do
225
+ it "should default to asc" do
226
+ @page.add_dynamic_attribute(:price, "integer")
227
+ @page.price = 2
228
+ @page.save
229
+ @page_2 = Page.create
230
+ @page_2.add_dynamic_attribute(:price, "integer")
231
+ @page_2.price = 1
232
+ @page_2.save
233
+ @page_3 = Page.create
234
+ @page_3.add_dynamic_attribute(:price, "float")
235
+ @page_3.price = 3.0
236
+ @page_3.save
237
+ @page_4 = Page.create
238
+ @page_4.add_dynamic_attribute(:price, "float")
239
+ @page_4.price = 3.5
240
+ @page_4.save
241
+
242
+ # What if different records have price but they are of different object_type?
243
+ # page_1.price is an "integer" and it's saved in "integer_value" column
244
+ # page_2.price is a "float" and it's saved in "float_value"
245
+ #
246
+ # How would you work on Page.order_datt("price")?
247
+ #
248
+ # Answer 1: we could require passing of the object_type in the order method
249
+ # This way, we know which column to look at => order_datt("price", "integer")
250
+ Page.order_datt("price", "integer").should == [@page_2, @page]
251
+ Page.order_datt("price DESC", "float").should == [@page_4, @page_3]
252
+ end
253
+ end
254
+
255
+ describe "where_datt" do
256
+ it "should automatically join in the datts table" do
257
+ @page.add_dynamic_attribute(:price, "float")
258
+ @page.price = 200.0
259
+ @page.add_dynamic_attribute(:farce, "boolean")
260
+ @page.farce = true
261
+ @page.save
262
+
263
+ @page_2 = Page.create
264
+ @page_2.add_dynamic_attribute(:price, "integer")
265
+ @page_2.add_dynamic_attribute(:farce, "boolean")
266
+ @page_2.farce = true
267
+ @page_2.price = 1
268
+ @page_2.save
269
+
270
+ @result = Page.where_datt(:price => 200.0, :farce => true)
271
+ @result.should include(@page)
272
+ @result.should_not include(@page_2)
273
+
274
+ #@result = Page.where_datt("price < :price", :price => 199.0)
275
+ #@result.should_not include(@page)
276
+ #@result.should include(@page_2)
277
+ end
278
+
279
+ it "should allow chaining with where scopes" do
280
+ @page.add_dynamic_attribute(:price, "integer")
281
+ @page.name = "aardvark"
282
+ @page.price = 200
283
+ @page.save
284
+
285
+ @page_2 = Page.create
286
+ @page_2.add_dynamic_attribute(:price, "integer")
287
+ @page_2.price = 200
288
+ @page_2.save
289
+
290
+ @results = Page.where_datt(:price => 200).where(:name => "aardvark")
291
+ @results.should include(@page)
292
+ @results.should_not include(@page_2)
293
+ end
294
+
295
+ it "should return an empty array if we're looking for a field that doesn't exist" do
296
+ Page.where_datt(:bogus_field => "none").should be_empty
297
+ end
298
+ end
299
+ end