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