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
@@ -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
|