has_custom_fields 0.0.5 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source :rubygems
2
+
3
+ gemspec
4
+
5
+ group :development do
6
+ gem 'ruby-debug19'
7
+ end
@@ -0,0 +1,114 @@
1
+ HasCustomFields
2
+ ========================
3
+
4
+ HasCustomFields provides you with a way to have multiple scoped dynamic key value,
5
+ attribute stores persisted in the database for any given model. These are also
6
+ known as the Entity-Attribute-Value model (EAV).
7
+
8
+
9
+ Installation
10
+ ------------------------
11
+
12
+ In your Gemfile:
13
+
14
+ gem "has_custom_fields"
15
+
16
+ Do a bundle install, then for each model you want to have custom fields (say User
17
+ with an organization scope for the fields and attributes) do the following:
18
+
19
+ $ rails generate has_custom_fields:install User organization
20
+
21
+ Then check and run the generated migrations
22
+
23
+ $ rake db:migrate
24
+
25
+ Then add the has_custom_fields class method to your model:
26
+
27
+ class User < ActiveRecord::Base
28
+ has_custom_fields :scopes => [:organization]
29
+ end
30
+
31
+ For Rails 2.x users please depend on the 0.0.5 version of the gem
32
+
33
+
34
+ Description
35
+ -------------------------
36
+
37
+ ### What is Entity-attribute-value model?
38
+
39
+ Entity-attribute-value model (EAV) is a data model that is used in circumstances
40
+ where the number of attributes (properties, parameters) that can be used to describe
41
+ a thing (an "entity" or "object") is potentially very vast, but the number that will
42
+ actually apply to a given entity is relatively modest.
43
+
44
+
45
+ Typical Problem
46
+ -------------------------
47
+
48
+ Say you have a contact management system, and each person in the database belongs
49
+ to an organisation. That organization may have a bunch of values that need to be
50
+ stored in the database, such as, primary contact name, or hot prospect?
51
+
52
+ Each of these could be stored as column values on the organization table, but
53
+ you could have 10-20 of these, and adding a new one would require a database change.
54
+
55
+ HasCustomFields allows you to define these key values simply with an attribute
56
+ type.
57
+
58
+
59
+ Example
60
+ -------------------------
61
+
62
+ Following on from the installation section above, we have a User with the following
63
+ custom fields setting:
64
+
65
+ class User < ActiveRecord::Base
66
+ has_custom_fields :scopes => [:organization]
67
+ end
68
+
69
+ We can now find the custom fields for the organization.
70
+
71
+ @organization = @user.organization
72
+ @organization_fields = User.custom_field_fields(:organization, @organization.id)
73
+
74
+ This returns an array full of UserFields that will look something like:
75
+
76
+ UserField id: 104, name: "High Potential", style: "checkbox", select_options: nil
77
+
78
+ Which could then be rendered out into a HTML page like so (logic in HTML for demonstration purposes, would be better to do this as a helper method)
79
+
80
+ <% @organization_fields.each do |field| %>
81
+ <p>
82
+ <% case field.style %>
83
+ <% when 'checkbox' %>
84
+ <%= f.check_box field.name %>
85
+ <% when 'text' %>
86
+ <%= f.text_field field.name, @user.custom_fields[:organization][@organization.id]['High Potential'] %>
87
+ <% end %>
88
+ </p>
89
+ <% end %>
90
+
91
+ Running the Specs
92
+ ========================
93
+
94
+ Ruby Environment
95
+ ------------------------
96
+
97
+ You should run the unit tests under ruby-1.9.2, a sample .rvmrc would be:
98
+
99
+ $ cat .rvmrc
100
+ rvm ruby-1.9.2@has_custom_fields --create
101
+
102
+ Once you have the right ruby, you should bundle install. Running the specs then
103
+ is done with:
104
+
105
+ $ rake spec
106
+
107
+ Creating the test database
108
+ ------------------------
109
+
110
+ The test databases will be created from the info specified in spec/db/database.yml.
111
+ Either change that file to match your database or change your database to
112
+ match that file.
113
+
114
+ Copyright (c) 2008 Marcus Wyatt, released under the MIT license
data/Rakefile CHANGED
@@ -1,121 +1,30 @@
1
- require 'rubygems'
2
- require 'rake'
3
-
1
+ #!/usr/bin/env rake
4
2
  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
3
+ require 'bundler/setup'
16
4
  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
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
25
6
  end
26
7
 
27
8
  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
9
+ require 'rdoc/task'
34
10
  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
11
+ require 'rdoc/rdoc'
12
+ require 'rake/task'
13
+ RDoc::Task = Rake::RDocTask
38
14
  end
39
15
 
40
- task :default => :test
41
-
42
- require 'rake/rdoctask'
43
- Rake::RDocTask.new do |rdoc|
44
- version = File.exist?('VERSION') ? File.read('VERSION') : ""
45
-
16
+ RDoc::Task.new(:rdoc) do |rdoc|
46
17
  rdoc.rdoc_dir = 'rdoc'
47
- rdoc.title = "constantations #{version}"
48
- rdoc.rdoc_files.include('README*')
18
+ rdoc.title = 'CustomFields'
19
+ rdoc.options << '--line-numbers'
20
+ rdoc.rdoc_files.include('README.rdoc')
49
21
  rdoc.rdoc_files.include('lib/**/*.rb')
50
22
  end
51
23
 
24
+ Bundler::GemHelper.install_tasks
52
25
 
26
+ task :spec do
27
+ sh("bundle exec rspec spec") { |ok, res| }
28
+ end
53
29
 
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
30
+ task :default => :spec
@@ -1,48 +1,41 @@
1
- # Generated by jeweler
2
- # DO NOT EDIT THIS FILE DIRECTLY
3
- # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
1
  # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib/', __FILE__)
3
+ $:.unshift lib unless $:.include?(lib)
4
+
5
+ require 'has_custom_fields/version'
5
6
 
6
7
  Gem::Specification.new do |s|
7
8
  s.name = %q{has_custom_fields}
8
- s.version = "0.0.5"
9
+ s.version = HasCustomFields::VERSION
9
10
 
10
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
12
  s.authors = ["kylejginavan"]
12
- s.date = %q{2012-01-27}
13
+ s.date = %q{2012-02-21}
13
14
  s.description = %q{Uses a vertical schema to add custom fields.}
14
15
  s.email = %q{kylejginavan@gmail.com}
15
16
  s.extra_rdoc_files = [
16
17
  "LICENSE",
17
- "README.rdoc"
18
+ "README.md"
18
19
  ]
19
20
  s.files = [
21
+ "Gemfile",
22
+ "init.rb",
23
+ "Rakefile",
20
24
  "LICENSE",
21
- "README.rdoc",
25
+ "README.md",
22
26
  "Rakefile",
23
- "SPECDOC",
24
- "VERSION",
25
27
  "has_custom_fields.gemspec",
26
- "has_custom_fields.tmproj",
27
- "lib/custom_fields/custom_field_base.rb",
28
+ "lib/has_custom_fields/base.rb",
29
+ "lib/has_custom_fields/class_methods.rb",
30
+ "lib/has_custom_fields/instance_methods.rb",
31
+ "lib/has_custom_fields/railtie.rb",
32
+ "lib/has_custom_fields/version.rb",
28
33
  "lib/has_custom_fields.rb",
29
- "spec/database.yml",
30
- "spec/debug.log",
31
- "spec/fixtures/document.rb",
32
- "spec/fixtures/people.yml",
33
- "spec/fixtures/person.rb",
34
- "spec/fixtures/person_contact_infos.yml",
35
- "spec/fixtures/post.rb",
36
- "spec/fixtures/post_attributes.yml",
37
- "spec/fixtures/posts.yml",
38
- "spec/fixtures/preference.rb",
39
- "spec/fixtures/preferences.yml",
40
- "spec/models/eav_model_with_no_arguments_spec.rb",
41
- "spec/models/eav_model_with_options_spec.rb",
42
- "spec/models/eav_validation_spec.rb",
43
- "spec/rcov.opts",
44
- "spec/schema.rb",
45
- "spec/spec.opts",
34
+ "spec/db/database.yml",
35
+ "spec/db/schema.rb",
36
+ "spec/test_models/user.rb",
37
+ "spec/test_models/organization.rb",
38
+ "spec/has_custom_fields_spec.rb",
46
39
  "spec/spec_helper.rb"
47
40
  ]
48
41
  s.homepage = %q{http://github.com/kylejginavan/has_custom_fields}
@@ -50,16 +43,8 @@ Gem::Specification.new do |s|
50
43
  s.rubygems_version = %q{1.5.2}
51
44
  s.summary = %q{The easy way to add custom fields to any Rails model.}
52
45
 
53
- if s.respond_to? :specification_version then
54
- s.specification_version = 3
55
-
56
- if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
57
- s.add_runtime_dependency(%q<builder>, [">= 0"])
58
- else
59
- s.add_dependency(%q<builder>, [">= 0"])
60
- end
61
- else
62
- s.add_dependency(%q<builder>, [">= 0"])
63
- end
46
+ s.add_dependency('activerecord', ['>= 3.1.0'])
47
+ s.add_development_dependency('rspec')
48
+ s.add_development_dependency('database_cleaner')
49
+ s.add_development_dependency('sqlite3')
64
50
  end
65
-
data/init.rb ADDED
@@ -0,0 +1,4 @@
1
+ require File.join(File.dirname(__FILE__), "lib", "has_custom_fields")
2
+ require "has_custom_fields/railtie"
3
+
4
+ HasCustomFields.insert
@@ -1,401 +1,50 @@
1
- require 'custom_fields/custom_field_base'
2
- include ::CustomFields
3
-
4
- module ActiveRecord # :nodoc:
5
- module Has # :nodoc:
6
- ##
7
- # HasCustomFields allow for the Entity-attribute-value model (EAV), also
8
- # known as object-attribute-value model and open schema on any of your ActiveRecord
9
- # models.
10
- #
11
- module CustomFields
12
-
13
- ALLOWABLE_TYPES = ['select', 'checkbox', 'text', 'date']
14
-
15
- Object.const_set('TagFacade', Class.new(Object)).class_eval do
16
- def initialize(object_with_custom_fields, scope, scope_id)
17
- @object = object_with_custom_fields
18
- @scope = scope
19
- @scope_id = scope_id
20
- end
21
- def [](tag)
22
- # puts "** Calling get_custom_field_attribute for #{@object.class},#{tag},#{@scope},#{@scope_id}"
23
- return @object.get_custom_field_attribute(tag, @scope, @scope_id)
24
- end
25
- end
26
-
27
- Object.const_set('ScopeIdFacade', Class.new(Object)).class_eval do
28
- def initialize(object_with_custom_fields, scope)
29
- @object = object_with_custom_fields
30
- @scope = scope
31
- end
32
- def [](scope_id)
33
- # puts "** Returning a TagFacade for #{@object.class},#{@scope},#{scope_id}"
34
- return TagFacade.new(@object, @scope, scope_id)
35
- end
36
- end
37
-
38
- Object.const_set('ScopeFacade', Class.new(Object)).class_eval do
39
- def initialize(object_with_custom_fields)
40
- @object = object_with_custom_fields
41
- end
42
- def [](scope)
43
- # puts "** Returning a ScopeIdFacade for #{@object.class},#{scope}"
44
- return ScopeIdFacade.new(@object, scope)
45
- end
46
- end
47
-
48
- def self.included(base) # :nodoc:
49
- base.extend ClassMethods
50
- end
51
-
52
- module ClassMethods
53
-
54
- ##
55
- # Will make the current class have eav behaviour.
56
- #
57
- # The following options are available on for has_custom_fields to modify
58
- # the behavior. Reasonable defaults are provided:
59
- #
60
- # * <tt>value_class_name</tt>:
61
- # The class for the related model. This defaults to the
62
- # model name prepended to "Attribute". So for a "User" model the class
63
- # name would be "UserAttribute". The class can actually exist (in that
64
- # case the model file will be loaded through Rails dependency system) or
65
- # if it does not exist a basic model will be dynamically defined for you.
66
- # This allows you to implement custom methods on the related class by
67
- # simply defining the class manually.
68
- # * <tt>table_name</tt>:
69
- # The table for the related model. This defaults to the
70
- # attribute model's table name.
71
- # * <tt>relationship_name</tt>:
72
- # This is the name of the actual has_many
73
- # relationship. Most of the type this relationship will only be used
74
- # indirectly but it is there if the user wants more raw access. This
75
- # defaults to the class name underscored then pluralized finally turned
76
- # into a symbol.
77
- # * <tt>foreign_key</tt>:
78
- # The key in the attribute table to relate back to the
79
- # model. This defaults to the model name underscored prepended to "_id"
80
- # * <tt>name_field</tt>:
81
- # The field which stores the name of the attribute in the related object
82
- # * <tt>value_field</tt>:
83
- # The field that stores the value in the related object
84
- def has_custom_fields(options = {})
85
-
86
- # Provide default options
87
- options[:fields_class_name] ||= self.name + 'Field'
88
- options[:fields_table_name] ||= options[:fields_class_name].tableize
89
- options[:fields_relationship_name] ||= options[:fields_class_name].underscore.to_sym
90
-
91
- options[:values_class_name] ||= self.name + 'Attribute'
92
- options[:values_table_name] ||= options[:values_class_name].tableize
93
- options[:relationship_name] ||= options[:values_class_name].tableize.to_sym
94
-
95
- options[:foreign_key] ||= self.name.foreign_key
96
- options[:base_foreign_key] ||= self.name.underscore.foreign_key
97
- options[:name_field] ||= 'name'
98
- options[:value_field] ||= 'value'
99
- options[:parent] = self.name
100
-
101
- ::Rails.logger.debug("OPTIONS: #{options.inspect}")
102
-
103
- # Init option storage if necessary
104
- cattr_accessor :custom_field_options
105
- self.custom_field_options ||= Hash.new
106
-
107
- # Return if already processed.
108
- return if self.custom_field_options.keys.include? options[:values_class_name]
109
-
110
- # Attempt to load related class. If not create it
111
- begin
112
- Object.const_get(options[:values_class_name])
113
- rescue
114
- Object.const_set(options[:fields_class_name],
115
- Class.new(::CustomFields::CustomFieldBase)).class_eval do
116
- set_table_name options[:fields_table_name]
117
- def self.reloadable? #:nodoc:
118
- false
119
- end
120
- end
121
- ::CustomFields.const_set(options[:fields_class_name], Object.const_get(options[:fields_class_name]))
122
-
123
- Object.const_set(options[:values_class_name],
124
- Class.new(ActiveRecord::Base)).class_eval do
125
- cattr_accessor :custom_field_options
126
- belongs_to options[:fields_relationship_name],
127
- :class_name => '::CustomFields::' + options[:fields_class_name].singularize
128
- alias_method :field, options[:fields_relationship_name]
129
-
130
- def self.reloadable? #:nodoc:
131
- false
132
- end
133
-
134
- validates_uniqueness_of options[:foreign_key].to_sym, :scope => "#{options[:fields_relationship_name]}_id".to_sym
135
-
136
- def validate
137
- field = self.field
138
- raise "Couldn't load field" if !field
139
-
140
- if field.style == "select" && !self.value.blank?
141
- # raise self.field.select_options.find{|f| f == self.value}.to_s
142
- if field.select_options.find{|f| f == self.value}.nil?
143
- raise "Invalid option: #{self.value}. Should be one of #{field.select_options.join(", ")}"
144
- self.errors.add_to_base("Invalid option: #{self.value}. Should be one of #{field.select_options.join(", ")}")
145
- return false
146
- end
147
- end
148
- end
149
- end
150
- ::CustomFields.const_set(options[:values_class_name], Object.const_get(options[:values_class_name]))
151
- end
152
-
153
- # Store options
154
- self.custom_field_options[self.name] = options
155
-
156
- # Only mix instance methods once
157
- unless self.included_modules.include?(ActiveRecord::Has::CustomFields::InstanceMethods)
158
- send :include, ActiveRecord::Has::CustomFields::InstanceMethods
159
- end
160
-
161
- # Modify attribute class
162
- attribute_class = Object.const_get(options[:values_class_name])
163
- base_class = self.name.underscore.to_sym
164
-
165
- attribute_class.class_eval do
166
- belongs_to base_class, :foreign_key => options[:base_foreign_key]
167
- alias_method :base, base_class # For generic access
168
- end
169
-
170
- # Modify main class
171
- class_eval do
172
- has_many options[:relationship_name],
173
- :class_name => options[:values_class_name],
174
- :table_name => options[:values_table_name],
175
- :foreign_key => options[:foreign_key],
176
- :dependent => :destroy
177
-
178
- # The following is only setup once
179
- unless method_defined? :read_attribute_without_custom_field_behavior
180
-
181
- # Carry out delayed actions before save
182
- after_validation :save_modified_custom_field_attributes, :on => :update
183
-
184
- private
185
-
186
- alias_method_chain :read_attribute, :custom_field_behavior
187
- alias_method_chain :write_attribute, :custom_field_behavior
188
- end
189
- end
190
-
191
- create_attribute_table
192
-
193
- end
194
-
195
- def custom_field_fields(scope, scope_id)
196
- options = custom_field_options[self.name]
197
- klass = Object.const_get(options[:fields_class_name])
198
- return klass.send("find_all_by_#{scope}_id", scope_id, :order => :id)
199
- end
200
-
201
- end
202
-
203
- module InstanceMethods
204
-
205
- def self.included(base) # :nodoc:
206
- base.extend ClassMethods
207
- end
208
-
209
- module ClassMethods
210
-
211
- ##
212
- # Rake migration task to create the versioned table using options passed to has_custom_fields
213
- #
214
- def create_attribute_table(options = {})
215
- options = custom_field_options[self.name]
216
- klass = Object.const_get(options[:fields_class_name])
217
- return if connection.tables.include?(options[:values_table_name])
218
-
219
- # todo: get the real pkey type and name
220
- scope_fkeys = options[:scopes].collect{|s| "#{s.to_s}_id"}
221
-
222
- ActiveRecord::Base.transaction do
223
-
224
- self.connection.create_table(options[:fields_table_name], options) do |t|
225
- t.string options[:name_field], :null => false, :limit => 63
226
- t.string :style, :null => false, :limit => 15
227
- t.string :select_options
228
- scope_fkeys.each do |s|
229
- t.integer s
230
- end
231
- t.timestamps
232
- end
233
- self.connection.add_index options[:fields_table_name], scope_fkeys + [options[:name_field]], :unique => true,
234
- :name => "#{options[:fields_table_name]}_unique_index"
235
-
236
- # add foreign keys for scoping tables
237
- options[:scopes].each do |s|
238
- self.connection.execute <<-FOO
239
- alter table #{options[:fields_table_name]}
240
- add foreign key (#{s.to_s}_id)
241
- references
242
- #{eval(s.to_s.classify).table_name}(#{eval(s.to_s.classify).primary_key})
243
- FOO
244
- end
245
-
246
- # add xor constraint
247
- if !options[:scopes].empty?
248
- self.connection.execute <<-FOO
249
- alter table #{options[:fields_table_name]} add constraint scopes_xor check
250
- (1 = #{options[:scopes].collect{|s| "(#{s.to_s}_id is not null)::integer"}.join(" + ")})
251
- FOO
252
- end
253
-
254
- self.connection.create_table(options[:values_table_name], options) do |t|
255
- t.integer options[:foreign_key], :null => false
256
- t.integer options[:fields_table_name].singularize.foreign_key, :null => false
257
- t.string options[:value_field], :null => false
258
- t.timestamps
259
- end
260
-
261
- self.connection.add_index options[:values_table_name], options[:foreign_key]
262
- self.connection.add_index options[:values_table_name], options[:fields_table_name].singularize.foreign_key
263
-
264
- self.connection.execute <<-FOO
265
- alter table #{options[:values_table_name]}
266
- add foreign key (#{options[:fields_table_name].singularize.foreign_key})
267
- references #{options[:fields_table_name]}(#{eval(options[:fields_class_name]).primary_key})
268
- FOO
269
- end
270
- end
271
-
272
- ##
273
- # Rake migration task to drop the attribute table
274
- #
275
- def drop_attribute_table(options = {})
276
- options = custom_field_options[self.name]
277
- self.connection.drop_table options[:values_table_name]
278
- end
279
-
280
- def drop_field_table(options = {})
281
- options = custom_field_options[self.name]
282
- self.connection.drop_table options[:fields_table_name]
283
- end
284
-
285
- end
286
-
287
- def get_custom_field_attribute(attribute_name, scope, scope_id)
288
- read_attribute_with_custom_field_behavior(attribute_name, scope, scope_id)
289
- end
290
-
291
- def set_custom_field_attribute(attribute_name, value, scope, scope_id)
292
- write_attribute_with_custom_field_behavior(attribute_name, value, scope, scope_id)
293
- end
294
-
295
- def custom_fields=(custom_fields_data)
296
- custom_fields_data.each do |scope, scoped_ids|
297
- scoped_ids.each do |scope_id, attrs|
298
- attrs.each do |k, v|
299
- self.set_custom_field_attribute(k, v, scope, scope_id)
300
- end
301
- end
302
- end
303
- end
304
-
305
- def custom_fields
306
- return ScopeFacade.new(self)
307
- end
308
-
309
- private
310
-
311
- ##
312
- # Called after validation on update so that eav attributes behave
313
- # like normal attributes in the fact that the database is not touched
314
- # until save is called.
315
- #
316
- def save_modified_custom_field_attributes
317
- return if @save_attrs.nil?
318
- @save_attrs.each do |s|
319
- if s.value.nil? || (s.respond_to?(:empty) && s.value.empty?)
320
- s.destroy if !s.new_record?
321
- else
322
- s.save
323
- end
324
- end
325
- @save_attrs = []
326
- end
327
-
328
- def get_value_object(attribute_name, scope, scope_id)
329
- ::Rails.logger.debug("scope/id is: #{scope}/#{scope_id}")
330
- options = custom_field_options[self.class.name]
331
- model_fkey = options[:foreign_key].singularize
332
- fields_class = options[:fields_class_name]
333
- values_class = options[:values_class_name]
334
- value_field = options[:value_field]
335
- fields_fkey = options[:fields_table_name].singularize.foreign_key
336
- fields = Object.const_get(fields_class)
337
- values = Object.const_get(values_class)
338
- ::Rails.logger.debug("fkey is: #{fields_fkey}")
339
- ::Rails.logger.debug("fields class: #{fields.to_s}")
340
- ::Rails.logger.debug("values class: #{values.to_s}")
341
- f = fields.send("find_by_name_and_#{scope}_id", attribute_name, scope_id)
342
- raise "No field #{attribute_name} for #{scope} #{scope_id}" if f.nil?
343
- ::Rails.logger.debug("field: #{f.inspect}")
344
- field_id = f.id
345
- model_id = self.id
346
- value_object = values.send("find_by_#{model_fkey}_and_#{fields_fkey}", model_id, field_id)
347
-
348
- if value_object.nil?
349
- value_object = values.new model_fkey => self.id,
350
- fields_fkey => f.id
351
- end
352
- return value_object
353
- end
354
-
355
- ##
356
- # Overrides ActiveRecord::Base#read_attribute
357
- #
358
- def read_attribute_with_custom_field_behavior(attribute_name, scope = nil, scope_id = nil)
359
- return read_attribute_without_custom_field_behavior(attribute_name) if scope.nil?
360
- value_object = get_value_object(attribute_name, scope, scope_id)
361
- case value_object.field.style
362
- when "date"
363
- ::Rails.logger.debug("reading date object: #{value_object.value}")
364
- return Date.parse(value_object.value) if value_object.value
365
- end
366
- return value_object.value
367
- end
368
-
369
- ##
370
- # Overrides ActiveRecord::Base#write_attribute
371
- #
372
- def write_attribute_with_custom_field_behavior(attribute_name, value, scope = nil, scope_id = nil)
373
- return write_attribute_without_custom_field_behavior(attribute_name, value) if scope.nil?
374
-
375
- ::Rails.logger.debug("attribute_name(#{attribute_name}) value(#{value.inspect}) scope(#{scope}) scope_id(#{scope_id})")
376
- value_object = get_value_object(attribute_name, scope, scope_id)
377
- case value_object.field.style
378
- when "date"
379
- ::Rails.logger.debug("date object: #{value["date(1i)"].to_i}, #{value["date(2i)"].to_i}, #{value["date(3i)"].to_i}")
380
- begin
381
- new_date = !value["date(1i)"].empty? && !value["date(2i)"].empty? && !value["date(3i)"].empty? ?
382
- Date.civil(value["date(1i)"].to_i, value["date(2i)"].to_i, value["date(3i)"].to_i) :
383
- nil
384
- rescue ArgumentError
385
- new_date = nil
386
- end
387
- value_object.send("value=", new_date) if value_object
388
- else
389
- value_object.send("value=", value) if value_object
390
- end
391
- @save_attrs ||= []
392
- @save_attrs << value_object
393
- end
1
+ require 'has_custom_fields/class_methods'
2
+ require 'has_custom_fields/instance_methods'
3
+ require 'has_custom_fields/base'
4
+ require 'has_custom_fields/railtie'
5
+
6
+ ##
7
+ # HasCustomFields allow for the Entity-attribute-value model (EAV), also
8
+ # known as object-attribute-value model and open schema on any of your ActiveRecord
9
+ # models.
10
+ #
11
+ module HasCustomFields
12
+
13
+ class InvalidScopeError < ActiveRecord::RecordNotFound; end
14
+
15
+ ALLOWABLE_TYPES = ['select', 'checkbox', 'text', 'date']
16
+
17
+ Object.const_set('TagFacade', Class.new(Object)).class_eval do
18
+ def initialize(object_with_custom_fields, scope, scope_id)
19
+ @object = object_with_custom_fields
20
+ @scope = scope
21
+ @scope_id = scope_id
22
+ end
23
+ def [](tag)
24
+ # puts "** Calling get_custom_field_attribute for #{@object.class},#{tag},#{@scope},#{@scope_id}"
25
+ return @object.get_custom_field_attribute(tag, @scope, @scope_id)
26
+ end
27
+ end
394
28
 
395
- end
29
+ Object.const_set('ScopeIdFacade', Class.new(Object)).class_eval do
30
+ def initialize(object_with_custom_fields, scope)
31
+ @object = object_with_custom_fields
32
+ @scope = scope
33
+ end
34
+ def [](scope_id)
35
+ # puts "** Returning a TagFacade for #{@object.class},#{@scope},#{scope_id}"
36
+ return TagFacade.new(@object, @scope, scope_id)
37
+ end
38
+ end
396
39
 
40
+ Object.const_set('ScopeFacade', Class.new(Object)).class_eval do
41
+ def initialize(object_with_custom_fields)
42
+ @object = object_with_custom_fields
43
+ end
44
+ def [](scope)
45
+ # puts "** Returning a ScopeIdFacade for #{@object.class},#{scope}"
46
+ return ScopeIdFacade.new(@object, scope)
397
47
  end
398
48
  end
399
- end
400
49
 
401
- ActiveRecord::Base.send :include, ActiveRecord::Has::CustomFields
50
+ end