custom_fields 0.0.0.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.
Files changed (34) hide show
  1. data/README +18 -0
  2. data/init.rb +2 -0
  3. data/lib/custom_fields.rb +28 -0
  4. data/lib/custom_fields/custom_fields_for.rb +50 -0
  5. data/lib/custom_fields/extensions/mongoid/associations/embeds_many.rb +31 -0
  6. data/lib/custom_fields/extensions/mongoid/associations/proxy.rb +20 -0
  7. data/lib/custom_fields/extensions/mongoid/associations/references_many.rb +33 -0
  8. data/lib/custom_fields/extensions/mongoid/hierarchy.rb +28 -0
  9. data/lib/custom_fields/field.rb +82 -0
  10. data/lib/custom_fields/proxy_class_enabler.rb +40 -0
  11. data/lib/custom_fields/types/boolean.rb +29 -0
  12. data/lib/custom_fields/types/category.rb +88 -0
  13. data/lib/custom_fields/types/date.rb +35 -0
  14. data/lib/custom_fields/types/default.rb +37 -0
  15. data/lib/custom_fields/types/file.rb +27 -0
  16. data/lib/custom_fields/types/string.rb +13 -0
  17. data/lib/custom_fields/types/text.rb +15 -0
  18. data/spec/integration/custom_fields_for_spec.rb +28 -0
  19. data/spec/integration/types/category_spec.rb +48 -0
  20. data/spec/integration/types/file_spec.rb +18 -0
  21. data/spec/models/person.rb +10 -0
  22. data/spec/models/project.rb +18 -0
  23. data/spec/models/task.rb +10 -0
  24. data/spec/spec_helper.rb +27 -0
  25. data/spec/support/carrierwave.rb +31 -0
  26. data/spec/support/mongoid.rb +6 -0
  27. data/spec/unit/custom_field_spec.rb +42 -0
  28. data/spec/unit/custom_fields_for_spec.rb +106 -0
  29. data/spec/unit/proxy_class_enabler_spec.rb +25 -0
  30. data/spec/unit/types/boolean_spec.rb +81 -0
  31. data/spec/unit/types/category_spec.rb +116 -0
  32. data/spec/unit/types/date_spec.rb +59 -0
  33. data/spec/unit/types/file_spec.rb +23 -0
  34. metadata +116 -0
data/README ADDED
@@ -0,0 +1,18 @@
1
+ CustomFields
2
+ ===========
3
+
4
+ Manage custom fields to a mongoid document or a collection. This module is one of the core features we implemented in our custom cms named Locomotive.
5
+
6
+ Requirements
7
+ =======
8
+
9
+ MongoDB 1.6 and mongoid 2.0.0.beta16
10
+
11
+
12
+ Example
13
+ =======
14
+
15
+ Coming soon but take a look of the tests to see what CustomFields is capable of.
16
+
17
+
18
+ Copyright (c) 2010 [Didier Lafforgue], released under the MIT license
data/init.rb ADDED
@@ -0,0 +1,2 @@
1
+ # Include hook code here
2
+ require File.dirname(__FILE__) + '/lib/custom_fields'
@@ -0,0 +1,28 @@
1
+ $:.unshift File.expand_path(File.dirname(__FILE__))
2
+
3
+ require 'active_support'
4
+ require 'carrierwave/orm/mongoid'
5
+
6
+ require 'custom_fields/extensions/mongoid/hierarchy'
7
+ require 'custom_fields/extensions/mongoid/associations/proxy'
8
+ require 'custom_fields/extensions/mongoid/associations/references_many'
9
+ require 'custom_fields/extensions/mongoid/associations/embeds_many'
10
+ require 'custom_fields/types/default'
11
+ require 'custom_fields/types/string'
12
+ require 'custom_fields/types/text'
13
+ require 'custom_fields/types/category'
14
+ require 'custom_fields/types/boolean'
15
+ require 'custom_fields/types/date'
16
+ require 'custom_fields/types/file'
17
+ require 'custom_fields/proxy_class_enabler'
18
+ require 'custom_fields/field'
19
+ require 'custom_fields/custom_fields_for'
20
+
21
+ module Mongoid
22
+ module CustomFields
23
+ extend ActiveSupport::Concern
24
+ included do
25
+ include ::CustomFields::CustomFieldsFor
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,50 @@
1
+ module CustomFields
2
+
3
+ module CustomFieldsFor
4
+
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ end
8
+
9
+ # Enhance an embedded collection by providing methods to manage custom fields
10
+ #
11
+ # class Company
12
+ # embeds_many :employees
13
+ # custom_fields_for :employees
14
+ # end
15
+ #
16
+ # class Employee
17
+ # embedded_in :company, :inverse_of => :employees
18
+ # field :name, String
19
+ # end
20
+ #
21
+ # company.employee_custom_fields.build :label => 'His/her position', :_alias => 'position', :kind => 'String'
22
+ #
23
+ # company.employees.build :name => 'Michael Scott', :position => 'Regional manager'
24
+ #
25
+ module ClassMethods
26
+
27
+ def custom_fields_for(collection_name)
28
+ singular_name = collection_name.to_s.singularize
29
+
30
+ class_eval <<-EOV
31
+ field :#{singular_name}_custom_fields_counter, :type => Integer, :default => 0
32
+
33
+ embeds_many :#{singular_name}_custom_fields, :class_name => "::CustomFields::Field"
34
+
35
+ validates_associated :#{singular_name}_custom_fields
36
+
37
+ accepts_nested_attributes_for :#{singular_name}_custom_fields, :allow_destroy => true
38
+
39
+ def ordered_#{singular_name}_custom_fields
40
+ self.#{singular_name}_custom_fields.sort { |a, b| (a.position || 0) <=> (b.position || 0) }
41
+ end
42
+
43
+ EOV
44
+ end
45
+
46
+ end
47
+
48
+ end
49
+
50
+ end
@@ -0,0 +1,31 @@
1
+ # encoding: utf-8
2
+ module Mongoid #:nodoc:
3
+ module Associations #:nodoc:
4
+ class EmbedsMany < Proxy
5
+
6
+ def initialize_with_custom_fields(parent, options, target_array = nil)
7
+ if custom_fields?(parent, options.name)
8
+ options = options.clone # 2 parent instances should not share the exact same option instance
9
+
10
+ custom_fields = parent.send(:"ordered_#{custom_fields_association_name(options.name)}")
11
+
12
+ klass = options.klass.to_klass_with_custom_fields(custom_fields)
13
+ klass._parent = parent
14
+ klass.association_name = options.name
15
+
16
+ options.instance_eval <<-EOF
17
+ def klass=(klass); @klass = klass; end
18
+ def klass; @klass || class_name.constantize; end
19
+ EOF
20
+
21
+ options.klass = klass
22
+ end
23
+
24
+ initialize_without_custom_fields(parent, options, target_array)
25
+ end
26
+
27
+ alias_method_chain :initialize, :custom_fields
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,20 @@
1
+ # encoding: utf-8
2
+ module Mongoid #:nodoc
3
+ module Associations #:nodoc
4
+ class Proxy #:nodoc
5
+
6
+ def custom_fields_association_name(association_name)
7
+ "#{association_name.to_s.singularize}_custom_fields".to_sym
8
+ end
9
+
10
+ def custom_fields?(object, association_name)
11
+ object.respond_to?(custom_fields_association_name(association_name))
12
+ end
13
+
14
+ def klass
15
+ @klass
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,33 @@
1
+ # encoding: utf-8
2
+ module Mongoid #:nodoc:
3
+ module Associations #:nodoc:
4
+ # Represents an relational one-to-many association with an object in a
5
+ # separate collection or database.
6
+ class ReferencesMany < Proxy
7
+
8
+ def initialize_with_custom_fields(parent, options, target_array = nil)
9
+ if custom_fields?(parent, options.name)
10
+ options = options.clone # 2 parent instances should not share the exact same option instance
11
+
12
+ custom_fields = parent.send(:"ordered_#{custom_fields_association_name(options.name)}")
13
+
14
+ klass = options.klass.to_klass_with_custom_fields(custom_fields)
15
+ klass._parent = parent
16
+ klass.association_name = options.name
17
+
18
+ options.instance_eval <<-EOF
19
+ def klass=(klass); @klass = klass; end
20
+ def klass; @klass || class_name.constantize; end
21
+ EOF
22
+
23
+ options.klass = klass
24
+ end
25
+
26
+ initialize_without_custom_fields(parent, options, target_array)
27
+ end
28
+
29
+ alias_method_chain :initialize, :custom_fields
30
+
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,28 @@
1
+ # encoding: utf-8
2
+ module Mongoid #:nodoc
3
+ module Hierarchy #:nodoc
4
+ module InstanceMethods
5
+
6
+ def parentize_with_custom_fields(object, association_name)
7
+ if association_name.to_s.ends_with?('_custom_fields')
8
+ self.singleton_class.associations = {}
9
+ self.singleton_class.embedded_in object.class.to_s.underscore.to_sym, :inverse_of => association_name
10
+ end
11
+
12
+ parentize_without_custom_fields(object, association_name)
13
+
14
+ if self.embedded? && self.instance_variable_get(:"@association_name").nil?
15
+ self.instance_variable_set(:"@association_name", association_name) # weird bug with proxy class
16
+ end
17
+
18
+ if association_name.to_s.ends_with?('_custom_fields')
19
+ self.send(:set_unique_name!)
20
+ self.send(:set_alias)
21
+ end
22
+ end
23
+
24
+ alias_method_chain :parentize, :custom_fields
25
+
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,82 @@
1
+ module CustomFields
2
+
3
+ class Field
4
+ include ::Mongoid::Document
5
+ include ::Mongoid::Timestamps
6
+
7
+ # types ##
8
+ include Types::Default
9
+ include Types::String
10
+ include Types::Text
11
+ include Types::Category
12
+ include Types::Boolean
13
+ include Types::Date
14
+ include Types::File
15
+
16
+ ## fields ##
17
+ field :label
18
+ field :_alias
19
+ field :_name
20
+ field :kind
21
+ field :hint
22
+ field :position, :type => Integer, :default => 0
23
+
24
+ ## validations ##
25
+ validates_presence_of :label, :kind
26
+ validate :uniqueness_of_label
27
+
28
+ ## methods ##
29
+
30
+ def field_type
31
+ self.class.field_types[self.kind.downcase.to_sym]
32
+ end
33
+
34
+ def apply(klass)
35
+ return unless self.valid?
36
+
37
+ klass.field self._name, :type => self.field_type if self.field_type
38
+
39
+ apply_method_name = :"apply_#{self.kind.downcase}_type"
40
+
41
+ if self.respond_to?(apply_method_name)
42
+ self.send(apply_method_name, klass)
43
+ else
44
+ apply_default_type(klass)
45
+ end
46
+ end
47
+
48
+ def safe_alias
49
+ self.set_alias
50
+ self._alias
51
+ end
52
+
53
+ protected
54
+
55
+ def uniqueness_of_label
56
+ duplicate = self.siblings.detect { |f| f.label == self.label && f._id != self._id }
57
+ if not duplicate.nil?
58
+ self.errors.add(:label, :taken)
59
+ end
60
+ end
61
+
62
+ def set_unique_name!
63
+ self._name ||= "custom_field_#{self.increment_counter!}"
64
+ end
65
+
66
+ def set_alias
67
+ return if self.label.blank? && self._alias.blank?
68
+ self._alias = (self._alias.blank? ? self.label : self._alias).parameterize('_').downcase
69
+ end
70
+
71
+ def increment_counter!
72
+ next_value = (self._parent.send(:"#{self.association_name}_counter") || 0) + 1
73
+ self._parent.send(:"#{self.association_name}_counter=", next_value)
74
+ next_value
75
+ end
76
+
77
+ def siblings
78
+ self._parent.send(self.association_name)
79
+ end
80
+ end
81
+
82
+ end
@@ -0,0 +1,40 @@
1
+ module CustomFields
2
+ module ProxyClassEnabler
3
+
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+
8
+ cattr_accessor :klass_with_custom_fields
9
+
10
+ def self.to_klass_with_custom_fields(fields)
11
+ return klass_with_custom_fields unless klass_with_custom_fields.nil?
12
+
13
+ klass = Class.new(self)
14
+ klass.class_eval <<-EOF
15
+ cattr_accessor :custom_fields, :_parent, :association_name
16
+
17
+ def self.model_name
18
+ @_model_name ||= ActiveModel::Name.new(self.superclass)
19
+ end
20
+
21
+ def custom_fields
22
+ self.class.custom_fields
23
+ end
24
+
25
+ def self.hereditary?
26
+ false
27
+ end
28
+ EOF
29
+
30
+ klass.custom_fields = fields
31
+
32
+ [*fields].each { |field| field.apply(klass) }
33
+
34
+ klass_with_custom_fields = klass
35
+ end
36
+
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,29 @@
1
+ module CustomFields
2
+ module Types
3
+ module Boolean
4
+
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ register_type :boolean
9
+ end
10
+
11
+ module InstanceMethods
12
+
13
+ def apply_boolean_type(klass)
14
+
15
+ klass.class_eval <<-EOF
16
+ alias :#{self.safe_alias}= :#{self._name}=
17
+
18
+ def #{self.safe_alias}
19
+ ::Boolean.set(read_attribute(:#{self._name}))
20
+ end
21
+ EOF
22
+
23
+ end
24
+
25
+ end
26
+
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,88 @@
1
+ module CustomFields
2
+ module Types
3
+ module Category
4
+
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ embeds_many :category_items, :class_name => 'CustomFields::Types::Category::Item'
9
+
10
+ validates_associated :category_items
11
+
12
+ accepts_nested_attributes_for :category_items, :allow_destroy => true
13
+
14
+ register_type :category, BSON::ObjectID
15
+ end
16
+
17
+ module InstanceMethods
18
+
19
+ def ordered_category_items
20
+ self.category_items.sort { |a, b| (a.position || 0) <=> (b.position || 0) }
21
+ end
22
+
23
+ def category_names
24
+ self.category_items.collect(&:name)
25
+ end
26
+
27
+ def category_ids
28
+ self.category_items.collect(&:_id)
29
+ end
30
+
31
+ def apply_category_type(klass)
32
+ klass.cattr_accessor :"#{self.safe_alias}_items"
33
+
34
+ klass.send("#{self.safe_alias}_items=", self.ordered_category_items)
35
+
36
+ klass.class_eval <<-EOF
37
+ def self.#{self.safe_alias}_names
38
+ self.#{self.safe_alias}_items.collect(&:name)
39
+ end
40
+
41
+ def self.group_by_#{self.safe_alias}(list_method = nil)
42
+ groups = (if self.embedded?
43
+ list_method ||= self.association_name
44
+ self._parent.send(list_method)
45
+ else
46
+ list_method ||= :all
47
+ self.send(list_method)
48
+ end).to_a.group_by(&:#{self._name})
49
+
50
+ self.#{self.safe_alias}_items.collect do |category|
51
+ {
52
+ :name => category.name,
53
+ :items => groups[category._id] || []
54
+ }.with_indifferent_access
55
+ end
56
+ end
57
+
58
+ def #{self.safe_alias}=(id)
59
+ category = self.class.#{self.safe_alias}_items.find { |item| item.name == id || item._id.to_s == id.to_s }
60
+ category_id = category ? category._id : nil
61
+
62
+ write_attribute(:#{self._name}, category_id)
63
+ end
64
+
65
+ def #{self.safe_alias}
66
+ category_id = read_attribute(:#{self._name})
67
+ category = self.class.#{self.safe_alias}_items.find { |item| item._id == category_id }
68
+ category ? category.name : nil
69
+ end
70
+ EOF
71
+ end
72
+
73
+ end
74
+
75
+ class Item
76
+
77
+ include Mongoid::Document
78
+
79
+ field :name
80
+ field :position, :type => Integer, :default => 0
81
+
82
+ embedded_in :custom_field, :inverse_of => :category_items
83
+
84
+ validates_presence_of :name
85
+ end
86
+ end
87
+ end
88
+ end