has_custom_fields 0.0.5 → 0.1.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/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