has_custom_fields 0.0.5 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +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
@@ -0,0 +1,20 @@
|
|
1
|
+
module HasCustomFields
|
2
|
+
class Base < ActiveRecord::Base
|
3
|
+
self.abstract_class = true
|
4
|
+
validates_presence_of :name,
|
5
|
+
:message => 'Please specify the field name.'
|
6
|
+
validates_presence_of :select_options_data,
|
7
|
+
:if => "self.style.to_sym == :select",
|
8
|
+
:message => "You must enter options for the selection"
|
9
|
+
|
10
|
+
def select_options_data
|
11
|
+
(self.related_select_options.collect{|o| o.option } || [])
|
12
|
+
end
|
13
|
+
|
14
|
+
def select_options_data=(data)
|
15
|
+
self.select_options = data.split(",").collect{|f| f.strip}
|
16
|
+
end
|
17
|
+
|
18
|
+
#scope :find_all_by_scope, lambda {|scope| {where("#{scope}_id = #{self.id}")}}
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,212 @@
|
|
1
|
+
module HasCustomFields
|
2
|
+
module ClassMethods
|
3
|
+
|
4
|
+
##
|
5
|
+
# Will make the current class have eav behaviour.
|
6
|
+
#
|
7
|
+
# The following options are available on for has_custom_fields to modify
|
8
|
+
# the behavior. Reasonable defaults are provided:
|
9
|
+
#
|
10
|
+
# * <tt>value_class_name</tt>:
|
11
|
+
# The class for the related model. This defaults to the
|
12
|
+
# model name prepended to "Attribute". So for a "User" model the class
|
13
|
+
# name would be "UserAttribute". The class can actually exist (in that
|
14
|
+
# case the model file will be loaded through Rails dependency system) or
|
15
|
+
# if it does not exist a basic model will be dynamically defined for you.
|
16
|
+
# This allows you to implement custom methods on the related class by
|
17
|
+
# simply defining the class manually.
|
18
|
+
# * <tt>table_name</tt>:
|
19
|
+
# The table for the related model. This defaults to the
|
20
|
+
# attribute model's table name.
|
21
|
+
# * <tt>relationship_name</tt>:
|
22
|
+
# This is the name of the actual has_many
|
23
|
+
# relationship. Most of the type this relationship will only be used
|
24
|
+
# indirectly but it is there if the user wants more raw access. This
|
25
|
+
# defaults to the class name underscored then pluralized finally turned
|
26
|
+
# into a symbol.
|
27
|
+
# * <tt>foreign_key</tt>:
|
28
|
+
# The key in the attribute table to relate back to the
|
29
|
+
# model. This defaults to the model name underscored prepended to "_id"
|
30
|
+
# * <tt>name_field</tt>:
|
31
|
+
# The field which stores the name of the attribute in the related object
|
32
|
+
# * <tt>value_field</tt>:
|
33
|
+
# The field that stores the value in the related object
|
34
|
+
def has_custom_fields(options = {})
|
35
|
+
|
36
|
+
unless options[:scopes].respond_to?(:each)
|
37
|
+
raise ArgumentError, 'Must define :scope => [] on the has_custom_fields class method'
|
38
|
+
end
|
39
|
+
|
40
|
+
# Provide default options
|
41
|
+
options[:fields_class_name] ||= self.name + 'Field'
|
42
|
+
options[:fields_table_name] ||= options[:fields_class_name].tableize
|
43
|
+
options[:fields_relationship_name] ||= options[:fields_class_name].underscore.to_sym
|
44
|
+
|
45
|
+
options[:values_class_name] ||= self.name + 'Attribute'
|
46
|
+
options[:values_table_name] ||= options[:values_class_name].tableize
|
47
|
+
options[:relationship_name] ||= options[:values_class_name].tableize.to_sym
|
48
|
+
|
49
|
+
options[:select_options_class_name] ||= self.name + "FieldSelectOption"
|
50
|
+
options[:select_options_table_name] ||= options[:select_options_class_name].tableize
|
51
|
+
options[:select_options_relationship_name] ||= options[:select_options_class_name].pluralize.underscore.to_sym
|
52
|
+
|
53
|
+
options[:foreign_key] ||= self.name.foreign_key
|
54
|
+
options[:base_foreign_key] ||= self.name.underscore.foreign_key
|
55
|
+
options[:name_field] ||= 'name'
|
56
|
+
options[:value_field] ||= 'value'
|
57
|
+
options[:parent] = self.name
|
58
|
+
|
59
|
+
HasCustomFields.log(:debug, "OPTIONS: #{options.inspect}")
|
60
|
+
|
61
|
+
# Init option storage if necessary
|
62
|
+
cattr_accessor :custom_field_options
|
63
|
+
self.custom_field_options ||= Hash.new
|
64
|
+
|
65
|
+
# Return if already processed.
|
66
|
+
return if self.custom_field_options.keys.include? options[:values_class_name]
|
67
|
+
|
68
|
+
# Attempt to load ModelField related class. If not create it
|
69
|
+
begin
|
70
|
+
Object.const_get(options[:fields_class_name])
|
71
|
+
rescue
|
72
|
+
HasCustomFields.create_associated_fields_class(options)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Attempt to load ModelAttribute related class. If not create it
|
76
|
+
begin
|
77
|
+
Object.const_get(options[:values_class_name])
|
78
|
+
rescue
|
79
|
+
HasCustomFields.create_associated_values_class(options)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Attempt to load ModelFieldSelectOption related class. If not create it
|
83
|
+
begin
|
84
|
+
Object.const_get(options[:select_options_class_name])
|
85
|
+
rescue
|
86
|
+
HasCustomFields.create_associated_select_options_class(options)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Store options
|
90
|
+
self.custom_field_options[self.name] = options
|
91
|
+
|
92
|
+
# Modify attribute class
|
93
|
+
attribute_class = Object.const_get(options[:values_class_name])
|
94
|
+
base_class = self.name.underscore.to_sym
|
95
|
+
|
96
|
+
attribute_class.class_eval do
|
97
|
+
belongs_to base_class, :foreign_key => options[:base_foreign_key]
|
98
|
+
alias_method :base, base_class # For generic access
|
99
|
+
end
|
100
|
+
|
101
|
+
# Modify main class
|
102
|
+
class_eval do
|
103
|
+
attr_accessible :custom_fields
|
104
|
+
has_many options[:relationship_name],
|
105
|
+
:class_name => options[:values_class_name],
|
106
|
+
:table_name => options[:values_table_name],
|
107
|
+
:foreign_key => options[:foreign_key],
|
108
|
+
:dependent => :destroy
|
109
|
+
|
110
|
+
# The following is only setup once
|
111
|
+
unless method_defined? :read_attribute_without_custom_field_behavior
|
112
|
+
|
113
|
+
# Carry out delayed actions before save
|
114
|
+
after_validation :save_modified_custom_field_attributes, :on => :update
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
alias_method_chain :read_attribute, :custom_field_behavior
|
119
|
+
alias_method_chain :write_attribute, :custom_field_behavior
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def custom_field_fields(scope, scope_id)
|
125
|
+
options = custom_field_options[self.name]
|
126
|
+
klass = Object.const_get(options[:fields_class_name])
|
127
|
+
begin
|
128
|
+
return klass.send("find_all_by_#{scope}_id", scope_id, :order => :id)
|
129
|
+
rescue NoMethodError
|
130
|
+
parent_class = klass.to_s.sub('Field', '')
|
131
|
+
raise InvalidScopeError, "Class #{parent_class} does not have scope :#{scope} defined for has_custom_fields"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
|
137
|
+
def HasCustomFields.create_associated_values_class(options)
|
138
|
+
Object.const_set(options[:values_class_name],
|
139
|
+
Class.new(ActiveRecord::Base)).class_eval do
|
140
|
+
self.table_name = options[:values_table_name]
|
141
|
+
|
142
|
+
cattr_accessor :custom_field_options
|
143
|
+
belongs_to options[:fields_relationship_name],
|
144
|
+
:class_name => '::HasCustomFields::' + options[:fields_class_name].singularize
|
145
|
+
|
146
|
+
alias_method :field, options[:fields_relationship_name]
|
147
|
+
|
148
|
+
|
149
|
+
def self.reloadable? #:nodoc:
|
150
|
+
false
|
151
|
+
end
|
152
|
+
|
153
|
+
validates_uniqueness_of options[:foreign_key].to_sym, :scope => "#{options[:fields_relationship_name]}_id".to_sym
|
154
|
+
|
155
|
+
def validate
|
156
|
+
field = self.field
|
157
|
+
raise "Couldn't load field" if !field
|
158
|
+
|
159
|
+
if field.style == "select" && !self.value.blank?
|
160
|
+
# raise self.field.select_options.find{|f| f == self.value}.to_s
|
161
|
+
if field.select_options.find{|f| f == self.value}.nil?
|
162
|
+
raise "Invalid option: #{self.value}. Should be one of #{field.select_options.join(", ")}"
|
163
|
+
self.errors.add_to_base("Invalid option: #{self.value}. Should be one of #{field.select_options.join(", ")}")
|
164
|
+
return false
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
::HasCustomFields.const_set(options[:values_class_name], Object.const_get(options[:values_class_name]))
|
170
|
+
end
|
171
|
+
|
172
|
+
def HasCustomFields.create_associated_fields_class(options)
|
173
|
+
Object.const_set(options[:fields_class_name],
|
174
|
+
Class.new(::HasCustomFields::Base)).class_eval do
|
175
|
+
self.table_name = options[:fields_table_name]
|
176
|
+
has_many options[:select_options_relationship_name]
|
177
|
+
alias_method :related_select_options, options[:select_options_relationship_name]
|
178
|
+
def self.reloadable? #:nodoc:
|
179
|
+
false
|
180
|
+
end
|
181
|
+
|
182
|
+
scopes = options[:scopes].map { |f| f.to_s.foreign_key }
|
183
|
+
validates_uniqueness_of :name, :scope => scopes, :message => 'The field name is already taken.'
|
184
|
+
|
185
|
+
validates_inclusion_of :style, :in => ALLOWABLE_TYPES, :message => "Invalid style. Should be #{ALLOWABLE_TYPES.join(', ')}."
|
186
|
+
end
|
187
|
+
::HasCustomFields.const_set(options[:fields_class_name], Object.const_get(options[:fields_class_name]))
|
188
|
+
end
|
189
|
+
|
190
|
+
def HasCustomFields.create_associated_select_options_class(options)
|
191
|
+
Object.const_set(options[:select_options_class_name],
|
192
|
+
Class.new(ActiveRecord::Base)).class_eval do
|
193
|
+
self.table_name = options[:select_options_table_name]
|
194
|
+
belongs_to options[:fields_relationship_name], :class_name => options[:fields_relationship_name].to_s
|
195
|
+
|
196
|
+
validates_presence_of :option, :message => 'The select option cannot be blank'
|
197
|
+
|
198
|
+
end
|
199
|
+
::HasCustomFields.const_set(options[:select_options_class_name], Object.const_get(options[:select_options_class_name]))
|
200
|
+
end
|
201
|
+
|
202
|
+
def HasCustomFields.log(level, message)
|
203
|
+
if defined?(::Rails)
|
204
|
+
::Rails.logger.send(level, message)
|
205
|
+
else
|
206
|
+
if ENV['debug'] == 'debug'
|
207
|
+
STDOUT.puts("HasCustomFields #{level}, #{message}")
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
module HasCustomFields
|
2
|
+
module InstanceMethods
|
3
|
+
|
4
|
+
def get_custom_field_attribute(attribute_name, scope, scope_id)
|
5
|
+
read_attribute_with_custom_field_behavior(attribute_name, scope, scope_id)
|
6
|
+
end
|
7
|
+
|
8
|
+
def set_custom_field_attribute(attribute_name, value, scope, scope_id)
|
9
|
+
write_attribute_with_custom_field_behavior(attribute_name, value, scope, scope_id)
|
10
|
+
end
|
11
|
+
|
12
|
+
def custom_fields=(custom_fields_data)
|
13
|
+
custom_fields_data.each do |scope, scoped_ids|
|
14
|
+
scoped_ids.each do |scope_id, attrs|
|
15
|
+
attrs.each do |k, v|
|
16
|
+
if v.blank?
|
17
|
+
value_object = get_value_object(k, scope, scope_id)
|
18
|
+
value_object.delete
|
19
|
+
else
|
20
|
+
self.set_custom_field_attribute(k, v, scope, scope_id)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def custom_fields
|
28
|
+
return ScopeFacade.new(self)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
##
|
34
|
+
# Called after validation on update so that eav attributes behave
|
35
|
+
# like normal attributes in the fact that the database is not touched
|
36
|
+
# until save is called.
|
37
|
+
#
|
38
|
+
def save_modified_custom_field_attributes
|
39
|
+
return if @save_attrs.nil?
|
40
|
+
@save_attrs.each do |s|
|
41
|
+
if s.value.nil? || (s.respond_to?(:empty) && s.value.empty?)
|
42
|
+
s.destroy if !s.new_record?
|
43
|
+
else
|
44
|
+
s.save
|
45
|
+
end
|
46
|
+
end
|
47
|
+
@save_attrs = []
|
48
|
+
end
|
49
|
+
|
50
|
+
def get_value_object(attribute_name, scope, scope_id)
|
51
|
+
HasCustomFields.log(:debug, "scope/id is: #{scope}/#{scope_id}")
|
52
|
+
options = custom_field_options[self.class.name]
|
53
|
+
model_fkey = options[:foreign_key].singularize
|
54
|
+
fields_class = options[:fields_class_name]
|
55
|
+
values_class = options[:values_class_name]
|
56
|
+
value_field = options[:value_field]
|
57
|
+
fields_fkey = options[:fields_table_name].singularize.foreign_key
|
58
|
+
fields = Object.const_get(fields_class)
|
59
|
+
values = Object.const_get(values_class)
|
60
|
+
HasCustomFields.log(:debug, "fkey is: #{fields_fkey}")
|
61
|
+
HasCustomFields.log(:debug, "fields class: #{fields.to_s}")
|
62
|
+
HasCustomFields.log(:debug, "values class: #{values.to_s}")
|
63
|
+
HasCustomFields.log(:debug, "scope is: #{scope}")
|
64
|
+
HasCustomFields.log(:debug, "scope_id is: #{scope_id}")
|
65
|
+
HasCustomFields.log(:debug, "attribute_name is: #{attribute_name}")
|
66
|
+
|
67
|
+
f = fields.send("find_by_name_and_#{scope}_id", attribute_name, scope_id)
|
68
|
+
|
69
|
+
raise(ActiveRecord::RecordNotFound, "No field #{attribute_name} for #{scope} #{scope_id}") if f.nil?
|
70
|
+
|
71
|
+
HasCustomFields.log(:debug, "field: #{f.inspect}")
|
72
|
+
field_id = f.id
|
73
|
+
model_id = self.id
|
74
|
+
value_object = values.send("find_by_#{model_fkey}_and_#{fields_fkey}", model_id, field_id)
|
75
|
+
|
76
|
+
if value_object.nil?
|
77
|
+
value_object = values.new model_fkey => self.id,
|
78
|
+
fields_fkey => f.id
|
79
|
+
end
|
80
|
+
return value_object
|
81
|
+
end
|
82
|
+
|
83
|
+
##
|
84
|
+
# Overrides ActiveRecord::Base#read_attribute
|
85
|
+
#
|
86
|
+
def read_attribute_with_custom_field_behavior(attribute_name, scope = nil, scope_id = nil)
|
87
|
+
return read_attribute_without_custom_field_behavior(attribute_name) if scope.nil?
|
88
|
+
value_object = get_value_object(attribute_name, scope, scope_id)
|
89
|
+
case value_object.field.style
|
90
|
+
when "date"
|
91
|
+
HasCustomFields.log(:debug, "reading date object: #{value_object.value}")
|
92
|
+
return Date.parse(value_object.value) if value_object.value
|
93
|
+
end
|
94
|
+
return value_object.value
|
95
|
+
end
|
96
|
+
|
97
|
+
##
|
98
|
+
# Overrides ActiveRecord::Base#write_attribute
|
99
|
+
#
|
100
|
+
def write_attribute_with_custom_field_behavior(attribute_name, value, scope = nil, scope_id = nil)
|
101
|
+
return write_attribute_without_custom_field_behavior(attribute_name, value) if scope.nil?
|
102
|
+
|
103
|
+
HasCustomFields.log(:debug, "attribute_name(#{attribute_name}) value(#{value.inspect}) scope(#{scope}) scope_id(#{scope_id})")
|
104
|
+
value_object = get_value_object(attribute_name, scope, scope_id)
|
105
|
+
case value_object.field.style
|
106
|
+
when "date"
|
107
|
+
HasCustomFields.log(:debug, "date object: #{value["date(1i)"].to_i}, #{value["date(2i)"].to_i}, #{value["date(3i)"].to_i}")
|
108
|
+
begin
|
109
|
+
new_date = !value["date(1i)"].empty? && !value["date(2i)"].empty? && !value["date(3i)"].empty? ?
|
110
|
+
Date.civil(value["date(1i)"].to_i, value["date(2i)"].to_i, value["date(3i)"].to_i) :
|
111
|
+
nil
|
112
|
+
rescue ArgumentError
|
113
|
+
new_date = nil
|
114
|
+
end
|
115
|
+
value_object.send("value=", new_date) if value_object
|
116
|
+
else
|
117
|
+
value_object.send("value=", value) if value_object
|
118
|
+
end
|
119
|
+
@save_attrs ||= []
|
120
|
+
@save_attrs << value_object
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'has_custom_fields'
|
2
|
+
|
3
|
+
module HasCustomFields
|
4
|
+
|
5
|
+
def HasCustomFields.insert
|
6
|
+
unless ActiveRecord::Base.included_modules.include?(HasCustomFields::InstanceMethods)
|
7
|
+
ActiveRecord::Base.extend HasCustomFields::ClassMethods
|
8
|
+
ActiveRecord::Base.send :include, HasCustomFields::InstanceMethods
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
if defined?(::Rails::Railtie)
|
13
|
+
require "rails"
|
14
|
+
|
15
|
+
class Railtie < Rails::Railtie
|
16
|
+
initializer "has_custom_fields.extend_active_record" do
|
17
|
+
ActiveSupport.on_load(:active_record) do
|
18
|
+
HasCustomFields.insert
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
else
|
23
|
+
HasCustomFields.insert
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
sqlite:
|
2
|
+
adapter: sqlite
|
3
|
+
database: spec/db/test.sqlite
|
4
|
+
|
5
|
+
sqlite3:
|
6
|
+
adapter: sqlite3
|
7
|
+
database: spec/db/test.sqlite3
|
8
|
+
|
9
|
+
postgresql:
|
10
|
+
adapter: postgresql
|
11
|
+
username: postgres
|
12
|
+
password: postgres
|
13
|
+
database: has_custom_fields_plugin_test
|
14
|
+
min_messages: ERROR
|
15
|
+
|
16
|
+
mysql:
|
17
|
+
adapter: mysql
|
18
|
+
host: localhost
|
19
|
+
username: root
|
20
|
+
password:
|
21
|
+
database: has_custom_fields_plugin_test
|
data/spec/db/schema.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
# require File.join(File.dirname(__FILE__), 'fixtures/document')
|
2
|
+
|
3
|
+
ActiveRecord::Schema.define(:version => 0) do
|
4
|
+
|
5
|
+
create_table "organizations", :force => true do |t|
|
6
|
+
t.string "name", :null => false
|
7
|
+
t.string "category"
|
8
|
+
t.datetime "created_at"
|
9
|
+
t.datetime "updated_at"
|
10
|
+
end
|
11
|
+
|
12
|
+
create_table "users", :force => true do |t|
|
13
|
+
t.string "name"
|
14
|
+
t.string "email"
|
15
|
+
t.integer "organization_id"
|
16
|
+
t.datetime "created_at"
|
17
|
+
t.datetime "updated_at"
|
18
|
+
end
|
19
|
+
|
20
|
+
create_table "user_attributes", :force => true do |t|
|
21
|
+
t.integer "user_id", :null => false
|
22
|
+
t.integer "user_field_id", :null => false
|
23
|
+
t.string "value", :null => false
|
24
|
+
t.datetime "created_at"
|
25
|
+
t.datetime "updated_at"
|
26
|
+
end
|
27
|
+
|
28
|
+
create_table "user_fields", :force => true do |t|
|
29
|
+
t.string "name", :null => false, :limit => 63
|
30
|
+
t.string "style", :null => false, :limit => 15
|
31
|
+
t.integer "organization_id"
|
32
|
+
t.datetime "created_at"
|
33
|
+
t.datetime "updated_at"
|
34
|
+
end
|
35
|
+
|
36
|
+
create_table "user_field_select_options", :force => true do |t|
|
37
|
+
t.integer "user_field_id"
|
38
|
+
t.string "option"
|
39
|
+
t.datetime "created_at"
|
40
|
+
t.datetime "updated_at"
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|