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 +7 -0
- data/README.md +114 -0
- data/Rakefile +16 -107
- data/has_custom_fields.gemspec +25 -40
- data/init.rb +4 -0
- data/lib/has_custom_fields.rb +45 -396
- data/lib/has_custom_fields/base.rb +20 -0
- data/lib/has_custom_fields/class_methods.rb +212 -0
- data/lib/has_custom_fields/instance_methods.rb +124 -0
- data/lib/has_custom_fields/railtie.rb +25 -0
- data/lib/has_custom_fields/version.rb +3 -0
- data/spec/db/database.yml +21 -0
- data/spec/db/schema.rb +43 -0
- data/spec/has_custom_fields_spec.rb +150 -0
- data/spec/spec_helper.rb +28 -25
- data/spec/test_models/organization.rb +3 -0
- data/spec/test_models/user.rb +6 -0
- metadata +66 -31
- data/README.rdoc +0 -117
- data/SPECDOC +0 -23
- data/VERSION +0 -1
- data/has_custom_fields.tmproj +0 -63
- data/lib/custom_fields/custom_field_base.rb +0 -29
- data/spec/database.yml +0 -12
- data/spec/debug.log +0 -3211
- data/spec/fixtures/document.rb +0 -7
- data/spec/fixtures/people.yml +0 -4
- data/spec/fixtures/person.rb +0 -13
- data/spec/fixtures/person_contact_infos.yml +0 -10
- data/spec/fixtures/post.rb +0 -6
- data/spec/fixtures/post_attributes.yml +0 -15
- data/spec/fixtures/posts.yml +0 -9
- data/spec/fixtures/preference.rb +0 -5
- data/spec/fixtures/preferences.yml +0 -10
- data/spec/models/eav_model_with_no_arguments_spec.rb +0 -82
- data/spec/models/eav_model_with_options_spec.rb +0 -38
- data/spec/models/eav_validation_spec.rb +0 -12
- data/spec/rcov.opts +0 -1
- data/spec/schema.rb +0 -50
- data/spec/spec.opts +0 -2
data/Gemfile
ADDED
data/README.md
ADDED
@@ -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
|
-
|
2
|
-
require 'rake'
|
3
|
-
|
1
|
+
#!/usr/bin/env rake
|
4
2
|
begin
|
5
|
-
require '
|
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
|
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 '
|
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
|
-
|
36
|
-
|
37
|
-
|
11
|
+
require 'rdoc/rdoc'
|
12
|
+
require 'rake/task'
|
13
|
+
RDoc::Task = Rake::RDocTask
|
38
14
|
end
|
39
15
|
|
40
|
-
|
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
|
48
|
-
rdoc.
|
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
|
data/has_custom_fields.gemspec
CHANGED
@@ -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 =
|
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-
|
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.
|
18
|
+
"README.md"
|
18
19
|
]
|
19
20
|
s.files = [
|
21
|
+
"Gemfile",
|
22
|
+
"init.rb",
|
23
|
+
"Rakefile",
|
20
24
|
"LICENSE",
|
21
|
-
"README.
|
25
|
+
"README.md",
|
22
26
|
"Rakefile",
|
23
|
-
"SPECDOC",
|
24
|
-
"VERSION",
|
25
27
|
"has_custom_fields.gemspec",
|
26
|
-
"has_custom_fields.
|
27
|
-
"lib/
|
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/
|
31
|
-
"spec/
|
32
|
-
"spec/
|
33
|
-
"spec/
|
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
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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
data/lib/has_custom_fields.rb
CHANGED
@@ -1,401 +1,50 @@
|
|
1
|
-
require '
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
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
|
-
|
50
|
+
end
|