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.
@@ -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,3 @@
1
+ module HasCustomFields
2
+ VERSION = "0.1.1"
3
+ 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
@@ -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