custom_fields 0.0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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