has_custom_fields 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/LICENSE +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
|