has_custom_fields 0.0.1

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.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Marcus Wyatt
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,117 @@
1
+ HasCustomFields
2
+ ==============
3
+
4
+ HasCustomFields allow for the Entity-attribute-value model (EAV), also
5
+ known as object-attribute-value model and open schema on any of your ActiveRecord
6
+ models.
7
+
8
+ = What is Entity-attribute-value model?
9
+ Entity-attribute-value model (EAV) is a data model that is used in circumstances
10
+ where the number of attributes (properties, parameters) that can be used to describe
11
+ a thing (an "entity" or "object") is potentially very vast, but the number that will
12
+ actually apply to a given entity is relatively modest.
13
+
14
+ = Typical Problem
15
+ A good example of this is where you need to store
16
+ lots (possible hundreds) of optional attributes on an object. My typical
17
+ reference example is when you have a User object. You want to store the
18
+ user's preferences between sessions. Every search, sort, etc in your
19
+ application you want to keep track of so when the user visits that section
20
+ of the application again you can simply restore the display to how it was.
21
+
22
+ So your controller might have:
23
+
24
+ Project.find :all, :conditions => current_user.project_search,
25
+ :order => current_user.project_order
26
+
27
+ But there could be hundreds of these little attributes that you really don't
28
+ want to store directly on the user object. It would make your table have too
29
+ many columns so it would be too much of a pain to deal with. Also there might
30
+ be performance problems. So instead you might do something like
31
+ this:
32
+
33
+ class User < ActiveRecord::Base
34
+ has_many :preferences
35
+ end
36
+
37
+ class Preferences < ActiveRecord::Base
38
+ belongs_to :user
39
+ end
40
+
41
+ Now simply give the Preference model a "name" and "value" column and you are
42
+ set..... except this is now too complicated. To retrieve a attribute you will
43
+ need to do something like:
44
+
45
+ Project.find :all,
46
+ :conditions => current_user.preferences.find_by_name('project_search').value,
47
+ :order => current_user.preferences.find_by_name('project_order').value
48
+
49
+ Sure you could fix this through a few methods on your model. But what about
50
+ saving?
51
+
52
+ current_user.preferences.create :name => 'project_search',
53
+ :value => "lastname LIKE 'jones%'"
54
+ current_user.preferences.create :name => 'project_order',
55
+ :value => "name"
56
+
57
+ Again this seems to much. Again we could add some methods to our model to
58
+ make this simpler but do we want to do this on every model. NO! So instead
59
+ we use this plugin which does everything for us.
60
+
61
+ = Capabilities
62
+
63
+ The HasCustomFields plugin is capable of modeling this problem in a intuitive
64
+ way. Instead of having to deal with a related model you treat all attributes
65
+ (both on the model and related) as if they are all on the model. The plugin
66
+ will try to save all attributes to the model (normal ActiveRecord behavior)
67
+ but if there is no column for an attribute it will try to save it to a
68
+ related model whose purpose is to store these many sparsely populated
69
+ attributes.
70
+
71
+ The main design goals are:
72
+
73
+ * Have the eav attributes feel like normal attributes. Simple gets and sets
74
+ will add and remove records from the related model.
75
+ * Allow for more than one related model. So for example on my User model I might
76
+ have some eav behavior going into a contact_info table while others are
77
+ going in a user_preferences table.
78
+ * Allow a model to determine what a valid eav attribute is for a given
79
+ related model so our model still can generate a NoMethodError.
80
+
81
+ Example
82
+ =======
83
+
84
+ Will make the current class have eav behaviour.
85
+
86
+ class Post < ActiveRecord::Base
87
+ has_custom_field_behavior
88
+ end
89
+ post = Post.find_by_title 'hello world'
90
+ puts "My post intro is: #{post.intro}"
91
+ post.teaser = 'An awesome introduction to the blog'
92
+ post.save
93
+
94
+ The above example should work even though "intro" and "teaser" are not
95
+ attributes on the Post model.
96
+
97
+ = Installation
98
+
99
+ ./script/plugin install acts_as_custom_field_model
100
+
101
+ = RUNNING UNIT TESTS
102
+
103
+ == Creating the test database
104
+
105
+ The test databases will be created from the info specified in test/database.yml.
106
+ Either change that file to match your database or change your database to
107
+ match that file.
108
+
109
+ == Running with Rake
110
+
111
+ The easiest way to run the unit tests is through Rake. By default sqlite3
112
+ will be the database run. Just change your env variable DB to be the database
113
+ adaptor (specified in database.yml) that you want to use. The database and
114
+ permissions must already be setup but the tables will be created for you
115
+ from schema.rb.
116
+
117
+ Copyright (c) 2008 Marcus Wyatt, released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,121 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "has_custom_fields"
8
+ gem.summary = %Q{The easy way to add custom fields to any Rails model.}
9
+ gem.description = %Q{Uses a vertical schema to add custom fields.}
10
+ gem.email = "kylejginavan@gmail.com"
11
+ gem.homepage = "http://github.com/kylejginavan/has_custom_fields"
12
+ gem.add_dependency('builder')
13
+ gem.authors = ["kylejginavan"]
14
+ end
15
+ Jeweler::GemcutterTasks.new
16
+ rescue LoadError
17
+ puts "has_custom_fields (or a dependency) not available. Install it with: gem install has_custom_fields"
18
+ end
19
+
20
+ require 'rake/testtask'
21
+ Rake::TestTask.new(:test) do |test|
22
+ test.libs << 'test'
23
+ test.pattern = 'test/**/test_*.rb'
24
+ test.verbose = true
25
+ end
26
+
27
+ begin
28
+ require 'rcov/rcovtask'
29
+ Rcov::RcovTask.new do |test|
30
+ test.libs << 'test'
31
+ test.pattern = 'test/**/test_*.rb'
32
+ test.verbose = true
33
+ end
34
+ rescue LoadError
35
+ task :rcov do
36
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
37
+ end
38
+ end
39
+
40
+ task :default => :test
41
+
42
+ require 'rake/rdoctask'
43
+ Rake::RDocTask.new do |rdoc|
44
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
45
+
46
+ rdoc.rdoc_dir = 'rdoc'
47
+ rdoc.title = "constantations #{version}"
48
+ rdoc.rdoc_files.include('README*')
49
+ rdoc.rdoc_files.include('lib/**/*.rb')
50
+ end
51
+
52
+
53
+
54
+
55
+ # require 'rake'
56
+ # require 'rake/testtask'
57
+ # require 'rake/rdoctask'
58
+ # require 'spec/rake/spectask'
59
+ # require 'spec/rake/verify_rcov'
60
+ #
61
+ # plugin_name = 'acts_as_custom_field_model'
62
+ #
63
+ # desc 'Default: run specs.'
64
+ # task :default => :spec
65
+ #
66
+ # desc "Run the specs for #{plugin_name}"
67
+ # Spec::Rake::SpecTask.new(:spec) do |t|
68
+ # t.spec_files = FileList['spec/**/*_spec.rb']
69
+ # t.spec_opts = ["--colour"]
70
+ # end
71
+ #
72
+ # namespace :spec do
73
+ # desc "Generate RCov report for #{plugin_name}"
74
+ # Spec::Rake::SpecTask.new(:rcov) do |t|
75
+ # t.spec_files = FileList['spec/**/*_spec.rb']
76
+ # t.rcov = true
77
+ # t.rcov_dir = 'doc/coverage'
78
+ # t.rcov_opts = ['--text-report', '--exclude', "spec/,rcov.rb,#{File.expand_path(File.join(File.dirname(__FILE__),'../../..'))}"]
79
+ # end
80
+ #
81
+ # namespace :rcov do
82
+ # desc "Verify RCov threshold for #{plugin_name}"
83
+ # RCov::VerifyTask.new(:verify => "spec:rcov") do |t|
84
+ # t.threshold = 100.0
85
+ # t.index_html = File.join(File.dirname(__FILE__), 'doc/coverage/index.html')
86
+ # end
87
+ # end
88
+ #
89
+ # desc "Generate specdoc for #{plugin_name}"
90
+ # Spec::Rake::SpecTask.new(:doc) do |t|
91
+ # t.spec_files = FileList['spec/**/*_spec.rb']
92
+ # t.spec_opts = ["--format", "specdoc:SPECDOC"]
93
+ # end
94
+ #
95
+ # namespace :doc do
96
+ # desc "Generate html specdoc for #{plugin_name}"
97
+ # Spec::Rake::SpecTask.new(:html => :rdoc) do |t|
98
+ # t.spec_files = FileList['spec/**/*_spec.rb']
99
+ # t.spec_opts = ["--format", "html:doc/rspec_report.html", "--diff"]
100
+ # end
101
+ # end
102
+ # end
103
+ #
104
+ # task :rdoc => :doc
105
+ # task "SPECDOC" => "spec:doc"
106
+ #
107
+ # desc "Generate rdoc for #{plugin_name}"
108
+ # Rake::RDocTask.new(:doc) do |t|
109
+ # t.rdoc_dir = 'doc'
110
+ # t.main = 'README.rdoc'
111
+ # t.title = "#{plugin_name}"
112
+ # t.template = ENV['RDOC_TEMPLATE']
113
+ # t.options = ['--line-numbers', '--inline-source', '--all']
114
+ # t.rdoc_files.include('README.rdoc', 'SPECDOC', 'MIT-LICENSE', 'CHANGELOG')
115
+ # t.rdoc_files.include('lib/**/*.rb')
116
+ # end
117
+ #
118
+ # namespace :doc do
119
+ # desc "Generate all documentation (rdoc, specdoc, specdoc html and rcov) for #{plugin_name}"
120
+ # task :all => ["spec:doc:html", "spec:doc", "spec:rcov", "doc"]
121
+ # end
data/SPECDOC ADDED
@@ -0,0 +1,23 @@
1
+
2
+ ActiveRecord Model annotated with 'has_custom_field_behavior' with no options in declaration
3
+ - should have many attributes
4
+ - should create new attribute on save
5
+ - should delete attribute
6
+ - should write eav attributes to attributes table
7
+ - should return nil when attribute does not exist
8
+ - should use method missing to make attribute seem as native property
9
+ - should read attributes using subscript notation
10
+ - should read the attribute when invoking 'read_attribute'
11
+
12
+ ActiveRecord Model annotated with 'has_custom_field_behavior' with options in declaration
13
+ - should be 'has_many' association on both sides
14
+ - should only allow restricted fields when specified (:fields => %w(phone aim icq))
15
+ - should raise 'NoMethodError' when attribute not in 'custom_field_attributes' method array
16
+ - should raise 'NoMethodError' when attribute does not satisfy 'is_custom_field_attribute?' method
17
+
18
+ Validations on ActiveRecord Model annotated with 'has_custom_field_behavior'
19
+ - should execute as normal (validates_presence_of)
20
+
21
+ Finished in 0.239663 seconds
22
+
23
+ 13 examples, 0 failures
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
@@ -0,0 +1,27 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>documents</key>
6
+ <array>
7
+ <dict>
8
+ <key>name</key>
9
+ <string>has_custom_fields</string>
10
+ <key>regexFolderFilter</key>
11
+ <string>!.*/(\.[^/]*|CVS|log|_darcs|_MTN|\{arch\}|blib|.*~\.nib|.*\.(framework|app|pbproj|pbxproj|xcode(proj)?|bundle))$</string>
12
+ <key>selected</key>
13
+ <true/>
14
+ <key>sourceDirectory</key>
15
+ <string></string>
16
+ </dict>
17
+ </array>
18
+ <key>fileHierarchyDrawerWidth</key>
19
+ <integer>200</integer>
20
+ <key>metaData</key>
21
+ <dict/>
22
+ <key>showFileHierarchyDrawer</key>
23
+ <true/>
24
+ <key>windowFrame</key>
25
+ <string>{{2049, 108}, {1203, 1047}}</string>
26
+ </dict>
27
+ </plist>
@@ -0,0 +1,28 @@
1
+ module CustomFields
2
+ class CustomFieldBase < ActiveRecord::Base
3
+
4
+ serialize :select_options
5
+ validates_presence_of :name,
6
+ :message => 'Please specify the field name.'
7
+ validates_presence_of :select_options_csv,
8
+ :if => "self.style.to_sym == :select",
9
+ :message => "You must enter options for the selection, separated by commas."
10
+
11
+ def self.inherited(chld)
12
+ super(chld)
13
+ chld.class_eval <<-FOO
14
+ validates_uniqueness_of :name, :scope => [:user_id, :organization_id], :message => 'The field name is already taken.'
15
+ validates_inclusion_of :style, :in => ALLOWABLE_TYPES, :message => "Invalid style. Should be #{ALLOWABLE_TYPES.join(', ')}."
16
+ FOO
17
+ end
18
+
19
+ def select_options_csv
20
+ (self.select_options || []).join(",")
21
+ end
22
+
23
+ def select_options_csv=(csv)
24
+ self.select_options = csv.split(",").collect{|f| f.strip}
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,398 @@
1
+ module ActiveRecord # :nodoc:
2
+ module Has # :nodoc:
3
+ ##
4
+ # HasCustomFields allow for the Entity-attribute-value model (EAV), also
5
+ # known as object-attribute-value model and open schema on any of your ActiveRecord
6
+ # models.
7
+ #
8
+ module CustomFields
9
+
10
+ ALLOWABLE_TYPES = ['select', 'boolean', 'text', 'date']
11
+
12
+ Object.const_set('TagFacade', Class.new(Object)).class_eval do
13
+ def initialize(object_with_custom_fields, scope, scope_id)
14
+ @object = object_with_custom_fields
15
+ @scope = scope
16
+ @scope_id = scope_id
17
+ end
18
+ def [](tag)
19
+ # puts "** Calling get_custom_field_attribute for #{@object.class},#{tag},#{@scope},#{@scope_id}"
20
+ return @object.get_custom_field_attribute(tag, @scope, @scope_id)
21
+ end
22
+ end
23
+
24
+ Object.const_set('ScopeIdFacade', Class.new(Object)).class_eval do
25
+ def initialize(object_with_custom_fields, scope)
26
+ @object = object_with_custom_fields
27
+ @scope = scope
28
+ end
29
+ def [](scope_id)
30
+ # puts "** Returning a TagFacade for #{@object.class},#{@scope},#{scope_id}"
31
+ return TagFacade.new(@object, @scope, scope_id)
32
+ end
33
+ end
34
+
35
+ Object.const_set('ScopeFacade', Class.new(Object)).class_eval do
36
+ def initialize(object_with_custom_fields)
37
+ @object = object_with_custom_fields
38
+ end
39
+ def [](scope)
40
+ # puts "** Returning a ScopeIdFacade for #{@object.class},#{scope}"
41
+ return ScopeIdFacade.new(@object, scope)
42
+ end
43
+ end
44
+
45
+ def self.included(base) # :nodoc:
46
+ base.extend ClassMethods
47
+ end
48
+
49
+ module ClassMethods
50
+
51
+ ##
52
+ # Will make the current class have eav behaviour.
53
+ #
54
+ # The following options are available on for has_custom_fields to modify
55
+ # the behavior. Reasonable defaults are provided:
56
+ #
57
+ # * <tt>value_class_name</tt>:
58
+ # The class for the related model. This defaults to the
59
+ # model name prepended to "Attribute". So for a "User" model the class
60
+ # name would be "UserAttribute". The class can actually exist (in that
61
+ # case the model file will be loaded through Rails dependency system) or
62
+ # if it does not exist a basic model will be dynamically defined for you.
63
+ # This allows you to implement custom methods on the related class by
64
+ # simply defining the class manually.
65
+ # * <tt>table_name</tt>:
66
+ # The table for the related model. This defaults to the
67
+ # attribute model's table name.
68
+ # * <tt>relationship_name</tt>:
69
+ # This is the name of the actual has_many
70
+ # relationship. Most of the type this relationship will only be used
71
+ # indirectly but it is there if the user wants more raw access. This
72
+ # defaults to the class name underscored then pluralized finally turned
73
+ # into a symbol.
74
+ # * <tt>foreign_key</tt>:
75
+ # The key in the attribute table to relate back to the
76
+ # model. This defaults to the model name underscored prepended to "_id"
77
+ # * <tt>name_field</tt>:
78
+ # The field which stores the name of the attribute in the related object
79
+ # * <tt>value_field</tt>:
80
+ # The field that stores the value in the related object
81
+ def has_custom_fields(options = {})
82
+
83
+ # Provide default options
84
+ options[:fields_class_name] ||= self.name + 'Field'
85
+ options[:fields_table_name] ||= options[:fields_class_name].tableize
86
+ options[:fields_relationship_name] ||= options[:fields_class_name].tableize.to_sym
87
+
88
+ options[:values_class_name] ||= self.name + 'Attribute'
89
+ options[:values_table_name] ||= options[:values_class_name].tableize
90
+ options[:relationship_name] ||= options[:values_class_name].tableize.to_sym
91
+
92
+ options[:foreign_key] ||= self.name.foreign_key
93
+ options[:base_foreign_key] ||= self.name.underscore.foreign_key
94
+ options[:name_field] ||= 'name'
95
+ options[:value_field] ||= 'value'
96
+ options[:parent] = self.name
97
+
98
+ ::Rails.logger.debug("OPTIONS: #{options.inspect}")
99
+ puts("OPTIONS: #{options.inspect}")
100
+
101
+ # Init option storage if necessary
102
+ cattr_accessor :custom_field_options
103
+ self.custom_field_options ||= Hash.new
104
+
105
+ # Return if already processed.
106
+ return if self.custom_field_options.keys.include? options[:values_class_name]
107
+
108
+ # Attempt to load related class. If not create it
109
+ begin
110
+ Object.const_get(options[:values_class_name])
111
+ rescue
112
+ Object.const_set(options[:fields_class_name],
113
+ Class.new(::CustomFields::CustomFieldBase)).class_eval do
114
+ set_table_name options[:fields_table_name]
115
+ def self.reloadable? #:nodoc:
116
+ false
117
+ end
118
+ end
119
+ ::CustomFields.const_set(options[:fields_class_name], Object.const_get(options[:fields_class_name]))
120
+
121
+ Object.const_set(options[:values_class_name],
122
+ Class.new(ActiveRecord::Base)).class_eval do
123
+ cattr_accessor :custom_field_options
124
+ belongs_to options[:fields_relationship_name],
125
+ :class_name => '::CustomFields::' + options[:fields_class_name].singularize
126
+ alias_method :field, options[:fields_relationship_name]
127
+
128
+ def self.reloadable? #:nodoc:
129
+ false
130
+ end
131
+
132
+ def validate
133
+ field = self.field
134
+ raise "Couldn't load field" if !field
135
+
136
+ if field.style == "select" && !self.value.blank?
137
+ # raise self.field.select_options.find{|f| f == self.value}.to_s
138
+ if field.select_options.find{|f| f == self.value}.nil?
139
+ raise "Invalid option: #{self.value}. Should be one of #{field.select_options.join(", ")}"
140
+ self.errors.add_to_base("Invalid option: #{self.value}. Should be one of #{field.select_options.join(", ")}")
141
+ return false
142
+ end
143
+ end
144
+ end
145
+ end
146
+ ::CustomFields.const_set(options[:values_class_name], Object.const_get(options[:values_class_name]))
147
+ end
148
+
149
+ # Store options
150
+ self.custom_field_options[self.name] = options
151
+
152
+ # Only mix instance methods once
153
+ unless self.included_modules.include?(ActiveRecord::Has::CustomFields::InstanceMethods)
154
+ send :include, ActiveRecord::Has::CustomFields::InstanceMethods
155
+ end
156
+
157
+ # Modify attribute class
158
+ attribute_class = Object.const_get(options[:values_class_name])
159
+ base_class = self.name.underscore.to_sym
160
+
161
+ attribute_class.class_eval do
162
+ belongs_to base_class, :foreign_key => options[:base_foreign_key]
163
+ alias_method :base, base_class # For generic access
164
+ end
165
+
166
+ # Modify main class
167
+ class_eval do
168
+ has_many options[:relationship_name],
169
+ :class_name => options[:values_class_name],
170
+ :table_name => options[:values_table_name],
171
+ :foreign_key => options[:foreign_key],
172
+ :dependent => :destroy
173
+
174
+ # The following is only setup once
175
+ unless method_defined? :read_attribute_without_custom_field_behavior
176
+
177
+ # Carry out delayed actions before save
178
+ after_validation :save_modified_custom_field_attributes, :on => :update
179
+
180
+ private
181
+
182
+ alias_method_chain :read_attribute, :custom_field_behavior
183
+ alias_method_chain :write_attribute, :custom_field_behavior
184
+ end
185
+ end
186
+
187
+ create_attribute_table
188
+
189
+ end
190
+
191
+ def custom_field_fields(scope, scope_id)
192
+ options = custom_field_options[self.name]
193
+ klass = Object.const_get(options[:fields_class_name])
194
+ return klass.send("find_all_by_#{scope}_id", scope_id, :order => :id)
195
+ end
196
+
197
+ end
198
+
199
+ module InstanceMethods
200
+
201
+ def self.included(base) # :nodoc:
202
+ base.extend ClassMethods
203
+ end
204
+
205
+ module ClassMethods
206
+
207
+ ##
208
+ # Rake migration task to create the versioned table using options passed to has_custom_fields
209
+ #
210
+ def create_attribute_table(options = {})
211
+ options = custom_field_options[self.name]
212
+ klass = Object.const_get(options[:fields_class_name])
213
+ return if connection.tables.include?(options[:values_table_name])
214
+
215
+ # todo: get the real pkey type and name
216
+ scope_fkeys = options[:scopes].collect{|s| "#{s.to_s}_id"}
217
+
218
+ ActiveRecord::Base.transaction do
219
+
220
+ self.connection.create_table(options[:fields_table_name], options) do |t|
221
+ t.string options[:name_field], :null => false
222
+ t.string :style, :null => false
223
+ t.string :select_options
224
+ scope_fkeys.each do |s|
225
+ t.integer s
226
+ end
227
+ t.timestamps
228
+ end
229
+ self.connection.add_index options[:fields_table_name], scope_fkeys + [options[:name_field]], :unique => true
230
+
231
+ # add foreign keys for scoping tables
232
+ options[:scopes].each do |s|
233
+ self.connection.execute <<-FOO
234
+ alter table #{options[:fields_table_name]}
235
+ add foreign key (#{s.to_s}_id)
236
+ references
237
+ #{eval(s.to_s.classify).table_name}(#{eval(s.to_s.classify).primary_key})
238
+ FOO
239
+ end
240
+
241
+ # add xor constraint
242
+ if !options[:scopes].empty?
243
+ self.connection.execute <<-FOO
244
+ alter table #{options[:fields_table_name]} add constraint scopes_xor check
245
+ (1 = #{options[:scopes].collect{|s| "(#{s.to_s}_id is not null)::integer"}.join(" + ")})
246
+ FOO
247
+ end
248
+
249
+ self.connection.create_table(options[:values_table_name], options) do |t|
250
+ t.integer options[:foreign_key], :null => false
251
+ t.integer options[:fields_table_name].foreign_key, :null => false
252
+ t.string options[:value_field], :null => false
253
+
254
+ t.timestamps
255
+ end
256
+
257
+ self.connection.add_index options[:values_table_name], options[:foreign_key]
258
+ self.connection.add_index options[:values_table_name], options[:fields_table_name].foreign_key
259
+
260
+ self.connection.execute <<-FOO
261
+ alter table #{options[:values_table_name]}
262
+ add foreign key (#{options[:fields_table_name].foreign_key})
263
+ references #{options[:fields_table_name]}(#{eval(options[:fields_class_name]).primary_key})
264
+ FOO
265
+ end
266
+ end
267
+
268
+ ##
269
+ # Rake migration task to drop the attribute table
270
+ #
271
+ def drop_attribute_table(options = {})
272
+ options = custom_field_options[self.name]
273
+ self.connection.drop_table options[:values_table_name]
274
+ end
275
+
276
+ def drop_field_table(options = {})
277
+ options = custom_field_options[self.name]
278
+ self.connection.drop_table options[:fields_table_name]
279
+ end
280
+
281
+ end
282
+
283
+ def get_custom_field_attribute(attribute_name, scope, scope_id)
284
+ read_attribute_with_custom_field_behavior(attribute_name, scope, scope_id)
285
+ end
286
+
287
+ def set_custom_field_attribute(attribute_name, value, scope, scope_id)
288
+ write_attribute_with_custom_field_behavior(attribute_name, value, scope, scope_id)
289
+ end
290
+
291
+ def custom_fields=(custom_fields_data)
292
+ custom_fields_data.each do |scope, scoped_ids|
293
+ scoped_ids.each do |scope_id, attrs|
294
+ attrs.each do |k, v|
295
+ self.set_custom_field_attribute(k, v, scope, scope_id)
296
+ end
297
+ end
298
+ end
299
+ end
300
+
301
+ def custom_fields
302
+ return ScopeFacade.new(self)
303
+ end
304
+
305
+ private
306
+
307
+ ##
308
+ # Called after validation on update so that eav attributes behave
309
+ # like normal attributes in the fact that the database is not touched
310
+ # until save is called.
311
+ #
312
+ def save_modified_custom_field_attributes
313
+ return if @save_attrs.nil?
314
+ @save_attrs.each do |s|
315
+ if s.value.nil? || (s.respond_to?(:empty) && s.value.empty?)
316
+ s.destroy if !s.new_record?
317
+ else
318
+ s.save
319
+ end
320
+ end
321
+ @save_attrs = []
322
+ end
323
+
324
+ def get_value_object(attribute_name, scope, scope_id)
325
+ ::Rails.logger.debug("scope/id is: #{scope}/#{scope_id}")
326
+ options = custom_field_options[self.class.name]
327
+ model_fkey = options[:foreign_key]
328
+ fields_class = options[:fields_class_name]
329
+ values_class = options[:values_class_name]
330
+ value_field = options[:value_field]
331
+ fields_fkey = options[:fields_table_name].foreign_key
332
+ fields = Object.const_get(fields_class)
333
+ values = Object.const_get(values_class)
334
+ ::Rails.logger.debug("fkey is: #{fields_fkey}")
335
+ ::Rails.logger.debug("fields class: #{fields.to_s}")
336
+ ::Rails.logger.debug("values class: #{values.to_s}")
337
+ f = fields.send("find_by_name_and_#{scope}_id", attribute_name, scope_id)
338
+ raise "No field #{attribute_name} for #{scope} #{scope_id}" if f.nil?
339
+ ::Rails.logger.debug("field: #{f.inspect}")
340
+ field_id = f.id
341
+ model_id = self.id
342
+ value_object = values.send("find_by_#{model_fkey}_and_#{fields_fkey}", model_id, field_id)
343
+
344
+ if value_object.nil?
345
+ value_object = values.new model_fkey => self.id,
346
+ fields_fkey => f.id
347
+ end
348
+
349
+ return value_object
350
+ end
351
+
352
+ ##
353
+ # Overrides ActiveRecord::Base#read_attribute
354
+ #
355
+ def read_attribute_with_custom_field_behavior(attribute_name, scope = nil, scope_id = nil)
356
+ return read_attribute_without_custom_field_behavior(attribute_name) if scope.nil?
357
+ value_object = get_value_object(attribute_name, scope, scope_id)
358
+ case value_object.field.style
359
+ when "date"
360
+ ::Rails.logger.debug("reading date object: #{value_object.value}")
361
+ return Date.parse(value_object.value) if value_object.value
362
+ end
363
+ return value_object.value
364
+ end
365
+
366
+ ##
367
+ # Overrides ActiveRecord::Base#write_attribute
368
+ #
369
+ def write_attribute_with_custom_field_behavior(attribute_name, value, scope = nil, scope_id = nil)
370
+ return write_attribute_without_custom_field_behavior(attribute_name, value) if scope.nil?
371
+
372
+ ::Rails.logger.debug("attribute_name(#{attribute_name}) value(#{value.inspect}) scope(#{scope}) scope_id(#{scope_id})")
373
+ value_object = get_value_object(attribute_name, scope, scope_id)
374
+ case value_object.field.style
375
+ when "date"
376
+ ::Rails.logger.debug("date object: #{value["date(1i)"].to_i}, #{value["date(2i)"].to_i}, #{value["date(3i)"].to_i}")
377
+ begin
378
+ new_date = !value["date(1i)"].empty? && !value["date(2i)"].empty? && !value["date(3i)"].empty? ?
379
+ Date.civil(value["date(1i)"].to_i, value["date(2i)"].to_i, value["date(3i)"].to_i) :
380
+ nil
381
+ rescue ArgumentError
382
+ new_date = nil
383
+ end
384
+ value_object.send("value=", new_date) if value_object
385
+ else
386
+ value_object.send("value=", value) if value_object
387
+ end
388
+ @save_attrs ||= []
389
+ @save_attrs << value_object
390
+ end
391
+
392
+ end
393
+
394
+ end
395
+ end
396
+ end
397
+
398
+ ActiveRecord::Base.send :include, ActiveRecord::Has::CustomFields