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