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 +20 -0
- data/README.rdoc +117 -0
- data/Rakefile +121 -0
- data/SPECDOC +23 -0
- data/VERSION +1 -0
- data/has_custom_fields.tmproj +27 -0
- data/lib/custom_fields/custom_field_base.rb +28 -0
- data/lib/has_custom_fields.rb +398 -0
- data/spec/database.yml +12 -0
- data/spec/debug.log +3211 -0
- data/spec/fixtures/document.rb +7 -0
- data/spec/fixtures/people.yml +4 -0
- data/spec/fixtures/person.rb +13 -0
- data/spec/fixtures/person_contact_infos.yml +10 -0
- data/spec/fixtures/post.rb +6 -0
- data/spec/fixtures/post_attributes.yml +15 -0
- data/spec/fixtures/posts.yml +9 -0
- data/spec/fixtures/preference.rb +5 -0
- data/spec/fixtures/preferences.yml +10 -0
- data/spec/models/eav_model_with_no_arguments_spec.rb +82 -0
- data/spec/models/eav_model_with_options_spec.rb +38 -0
- data/spec/models/eav_validation_spec.rb +12 -0
- data/spec/rcov.opts +1 -0
- data/spec/schema.rb +50 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +34 -0
- metadata +98 -0
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
|