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.
- data/README +18 -0
- data/init.rb +2 -0
- data/lib/custom_fields.rb +28 -0
- data/lib/custom_fields/custom_fields_for.rb +50 -0
- data/lib/custom_fields/extensions/mongoid/associations/embeds_many.rb +31 -0
- data/lib/custom_fields/extensions/mongoid/associations/proxy.rb +20 -0
- data/lib/custom_fields/extensions/mongoid/associations/references_many.rb +33 -0
- data/lib/custom_fields/extensions/mongoid/hierarchy.rb +28 -0
- data/lib/custom_fields/field.rb +82 -0
- data/lib/custom_fields/proxy_class_enabler.rb +40 -0
- data/lib/custom_fields/types/boolean.rb +29 -0
- data/lib/custom_fields/types/category.rb +88 -0
- data/lib/custom_fields/types/date.rb +35 -0
- data/lib/custom_fields/types/default.rb +37 -0
- data/lib/custom_fields/types/file.rb +27 -0
- data/lib/custom_fields/types/string.rb +13 -0
- data/lib/custom_fields/types/text.rb +15 -0
- data/spec/integration/custom_fields_for_spec.rb +28 -0
- data/spec/integration/types/category_spec.rb +48 -0
- data/spec/integration/types/file_spec.rb +18 -0
- data/spec/models/person.rb +10 -0
- data/spec/models/project.rb +18 -0
- data/spec/models/task.rb +10 -0
- data/spec/spec_helper.rb +27 -0
- data/spec/support/carrierwave.rb +31 -0
- data/spec/support/mongoid.rb +6 -0
- data/spec/unit/custom_field_spec.rb +42 -0
- data/spec/unit/custom_fields_for_spec.rb +106 -0
- data/spec/unit/proxy_class_enabler_spec.rb +25 -0
- data/spec/unit/types/boolean_spec.rb +81 -0
- data/spec/unit/types/category_spec.rb +116 -0
- data/spec/unit/types/date_spec.rb +59 -0
- data/spec/unit/types/file_spec.rb +23 -0
- 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,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
|