has_custom_fields 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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