tpitale-mongo_mapper 0.6.9

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 (75) hide show
  1. data/.gitignore +10 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +53 -0
  4. data/Rakefile +55 -0
  5. data/VERSION +1 -0
  6. data/bin/mmconsole +60 -0
  7. data/lib/mongo_mapper/associations/base.rb +110 -0
  8. data/lib/mongo_mapper/associations/belongs_to_polymorphic_proxy.rb +26 -0
  9. data/lib/mongo_mapper/associations/belongs_to_proxy.rb +21 -0
  10. data/lib/mongo_mapper/associations/collection.rb +19 -0
  11. data/lib/mongo_mapper/associations/many_documents_as_proxy.rb +26 -0
  12. data/lib/mongo_mapper/associations/many_documents_proxy.rb +115 -0
  13. data/lib/mongo_mapper/associations/many_embedded_polymorphic_proxy.rb +31 -0
  14. data/lib/mongo_mapper/associations/many_embedded_proxy.rb +54 -0
  15. data/lib/mongo_mapper/associations/many_polymorphic_proxy.rb +11 -0
  16. data/lib/mongo_mapper/associations/proxy.rb +113 -0
  17. data/lib/mongo_mapper/associations.rb +70 -0
  18. data/lib/mongo_mapper/callbacks.rb +109 -0
  19. data/lib/mongo_mapper/dirty.rb +136 -0
  20. data/lib/mongo_mapper/document.rb +472 -0
  21. data/lib/mongo_mapper/dynamic_finder.rb +74 -0
  22. data/lib/mongo_mapper/embedded_document.rb +384 -0
  23. data/lib/mongo_mapper/finder_options.rb +133 -0
  24. data/lib/mongo_mapper/key.rb +36 -0
  25. data/lib/mongo_mapper/observing.rb +50 -0
  26. data/lib/mongo_mapper/pagination.rb +55 -0
  27. data/lib/mongo_mapper/rails_compatibility/document.rb +15 -0
  28. data/lib/mongo_mapper/rails_compatibility/embedded_document.rb +27 -0
  29. data/lib/mongo_mapper/serialization.rb +54 -0
  30. data/lib/mongo_mapper/serializers/json_serializer.rb +92 -0
  31. data/lib/mongo_mapper/support.rb +206 -0
  32. data/lib/mongo_mapper/validations.rb +41 -0
  33. data/lib/mongo_mapper.rb +120 -0
  34. data/mongo_mapper.gemspec +173 -0
  35. data/specs.watchr +32 -0
  36. data/test/NOTE_ON_TESTING +1 -0
  37. data/test/functional/associations/test_belongs_to_polymorphic_proxy.rb +55 -0
  38. data/test/functional/associations/test_belongs_to_proxy.rb +48 -0
  39. data/test/functional/associations/test_many_documents_as_proxy.rb +246 -0
  40. data/test/functional/associations/test_many_documents_proxy.rb +387 -0
  41. data/test/functional/associations/test_many_embedded_polymorphic_proxy.rb +156 -0
  42. data/test/functional/associations/test_many_embedded_proxy.rb +192 -0
  43. data/test/functional/associations/test_many_polymorphic_proxy.rb +339 -0
  44. data/test/functional/test_associations.rb +44 -0
  45. data/test/functional/test_binary.rb +18 -0
  46. data/test/functional/test_callbacks.rb +85 -0
  47. data/test/functional/test_dirty.rb +159 -0
  48. data/test/functional/test_document.rb +1235 -0
  49. data/test/functional/test_embedded_document.rb +135 -0
  50. data/test/functional/test_logger.rb +20 -0
  51. data/test/functional/test_pagination.rb +95 -0
  52. data/test/functional/test_rails_compatibility.rb +25 -0
  53. data/test/functional/test_string_id_compatibility.rb +72 -0
  54. data/test/functional/test_validations.rb +378 -0
  55. data/test/models.rb +271 -0
  56. data/test/support/custom_matchers.rb +55 -0
  57. data/test/support/timing.rb +16 -0
  58. data/test/test_helper.rb +27 -0
  59. data/test/unit/associations/test_base.rb +166 -0
  60. data/test/unit/associations/test_proxy.rb +91 -0
  61. data/test/unit/serializers/test_json_serializer.rb +189 -0
  62. data/test/unit/test_document.rb +204 -0
  63. data/test/unit/test_dynamic_finder.rb +125 -0
  64. data/test/unit/test_embedded_document.rb +718 -0
  65. data/test/unit/test_finder_options.rb +296 -0
  66. data/test/unit/test_key.rb +172 -0
  67. data/test/unit/test_mongo_mapper.rb +65 -0
  68. data/test/unit/test_observing.rb +101 -0
  69. data/test/unit/test_pagination.rb +113 -0
  70. data/test/unit/test_rails_compatibility.rb +49 -0
  71. data/test/unit/test_serializations.rb +52 -0
  72. data/test/unit/test_support.rb +342 -0
  73. data/test/unit/test_time_zones.rb +40 -0
  74. data/test/unit/test_validations.rb +503 -0
  75. metadata +235 -0
@@ -0,0 +1,384 @@
1
+ module MongoMapper
2
+ module EmbeddedDocument
3
+ def self.included(model)
4
+ model.class_eval do
5
+ extend ClassMethods
6
+ include InstanceMethods
7
+
8
+ extend Associations::ClassMethods
9
+ include Associations::InstanceMethods
10
+
11
+ include RailsCompatibility::EmbeddedDocument
12
+ include Validatable
13
+ include Serialization
14
+
15
+ extend Validations::Macros
16
+
17
+ key :_id, ObjectId
18
+ attr_accessor :_root_document
19
+ end
20
+ end
21
+
22
+ module ClassMethods
23
+ def logger
24
+ MongoMapper.logger
25
+ end
26
+
27
+ def inherited(subclass)
28
+ unless subclass.embeddable?
29
+ subclass.set_collection_name(collection_name)
30
+ end
31
+
32
+ (@subclasses ||= []) << subclass
33
+ end
34
+
35
+ def subclasses
36
+ @subclasses
37
+ end
38
+
39
+ def keys
40
+ @keys ||= if parent = parent_model
41
+ parent.keys.dup
42
+ else
43
+ HashWithIndifferentAccess.new
44
+ end
45
+ end
46
+
47
+ def key(*args)
48
+ key = Key.new(*args)
49
+ keys[key.name] = key
50
+
51
+ create_accessors_for(key)
52
+ create_key_in_subclasses(*args)
53
+ create_validations_for(key)
54
+
55
+ key
56
+ end
57
+
58
+ def using_object_id?
59
+ object_id_key?(:_id)
60
+ end
61
+
62
+ def object_id_key?(name)
63
+ key = keys[name.to_s]
64
+ key && key.type == ObjectId
65
+ end
66
+
67
+ def embeddable?
68
+ !self.ancestors.include?(Document)
69
+ end
70
+
71
+ def parent_model
72
+ (ancestors - [self,EmbeddedDocument]).find do |parent_class|
73
+ parent_class.ancestors.include?(EmbeddedDocument)
74
+ end
75
+ end
76
+
77
+ def to_mongo(instance)
78
+ return nil if instance.nil?
79
+ instance.to_mongo
80
+ end
81
+
82
+ def from_mongo(value)
83
+ return nil if value.nil?
84
+ value.is_a?(self) ? value : initialize_doc(value)
85
+ end
86
+
87
+ private
88
+ def initialize_doc(doc)
89
+ begin
90
+ klass = doc['_type'].present? ? doc['_type'].constantize : self
91
+ klass.new(doc)
92
+ rescue NameError
93
+ new(doc)
94
+ end
95
+ end
96
+
97
+ def accessors_module
98
+ module_defined = if method(:const_defined?).arity == 1 # Ruby 1.9 compat check
99
+ const_defined?('MongoMapperKeys')
100
+ else
101
+ const_defined?('MongoMapperKeys', false)
102
+ end
103
+
104
+ if module_defined
105
+ const_get 'MongoMapperKeys'
106
+ else
107
+ const_set 'MongoMapperKeys', Module.new
108
+ end
109
+ end
110
+
111
+ def create_accessors_for(key)
112
+ accessors_module.module_eval <<-end_eval
113
+ def #{key.name}
114
+ read_attribute(:'#{key.name}')
115
+ end
116
+
117
+ def #{key.name}_before_typecast
118
+ read_attribute_before_typecast(:'#{key.name}')
119
+ end
120
+
121
+ def #{key.name}=(value)
122
+ write_attribute(:'#{key.name}', value)
123
+ end
124
+
125
+ def #{key.name}?
126
+ read_attribute(:#{key.name}).present?
127
+ end
128
+ end_eval
129
+ include accessors_module
130
+ end
131
+
132
+ def create_key_in_subclasses(*args)
133
+ return if subclasses.blank?
134
+
135
+ subclasses.each do |subclass|
136
+ subclass.key(*args)
137
+ end
138
+ end
139
+
140
+ def create_validations_for(key)
141
+ attribute = key.name.to_sym
142
+
143
+ if key.options[:required]
144
+ validates_presence_of(attribute)
145
+ end
146
+
147
+ if key.options[:unique]
148
+ validates_uniqueness_of(attribute)
149
+ end
150
+
151
+ if key.options[:numeric]
152
+ number_options = key.type == Integer ? {:only_integer => true} : {}
153
+ validates_numericality_of(attribute, number_options)
154
+ end
155
+
156
+ if key.options[:format]
157
+ validates_format_of(attribute, :with => key.options[:format])
158
+ end
159
+
160
+ if key.options[:length]
161
+ length_options = case key.options[:length]
162
+ when Integer
163
+ {:minimum => 0, :maximum => key.options[:length]}
164
+ when Range
165
+ {:within => key.options[:length]}
166
+ when Hash
167
+ key.options[:length]
168
+ end
169
+ validates_length_of(attribute, length_options)
170
+ end
171
+ end
172
+ end
173
+
174
+ module InstanceMethods
175
+ def initialize(attrs={})
176
+ unless attrs.nil?
177
+ associations.each do |name, association|
178
+ if collection = attrs.delete(name)
179
+ if association.many? && association.klass.embeddable?
180
+ root_document = attrs[:_root_document] || self
181
+ collection.each do |doc|
182
+ doc[:_root_document] = root_document
183
+ end
184
+ end
185
+ send("#{association.name}=", collection)
186
+ end
187
+ end
188
+
189
+ self.attributes = attrs
190
+
191
+ if respond_to?(:_type=) && self['_type'].blank?
192
+ self._type = self.class.name
193
+ end
194
+ end
195
+
196
+ if self.class.embeddable?
197
+ if read_attribute(:_id).blank?
198
+ write_attribute :_id, Mongo::ObjectID.new
199
+ @new_document = true
200
+ else
201
+ @new_document = false
202
+ end
203
+ end
204
+ end
205
+
206
+ def new?
207
+ !!@new_document
208
+ end
209
+
210
+ def to_param
211
+ id.to_s
212
+ end
213
+
214
+ def attributes=(attrs)
215
+ return if attrs.blank?
216
+ attrs.each_pair do |name, value|
217
+ writer_method = "#{name}="
218
+
219
+ if respond_to?(writer_method)
220
+ self.send(writer_method, value)
221
+ else
222
+ self[name.to_s] = value
223
+ end
224
+ end
225
+ end
226
+
227
+ def attributes
228
+ attrs = HashWithIndifferentAccess.new
229
+
230
+ embedded_keys.each do |key|
231
+ attrs[key.name] = read_attribute(key.name).try(:attributes)
232
+ end
233
+
234
+ non_embedded_keys.each do |key|
235
+ attrs[key.name] = read_attribute(key.name)
236
+ end
237
+
238
+ embedded_associations.each do |association|
239
+ documents = instance_variable_get(association.ivar)
240
+ next if documents.nil?
241
+ attrs[association.name] = documents.collect { |doc| doc.attributes }
242
+ end
243
+
244
+ attrs
245
+ end
246
+
247
+ def to_mongo
248
+ attrs = HashWithIndifferentAccess.new
249
+
250
+ _keys.each_pair do |name, key|
251
+ value = key.set(read_attribute(key.name))
252
+ attrs[name] = value unless value.nil?
253
+ end
254
+
255
+ embedded_associations.each do |association|
256
+ if documents = instance_variable_get(association.ivar)
257
+ attrs[association.name] = documents.map { |document| document.to_mongo }
258
+ end
259
+ end
260
+
261
+ attrs
262
+ end
263
+
264
+ def clone
265
+ clone_attributes = self.attributes
266
+ clone_attributes.delete("_id")
267
+ self.class.new(clone_attributes)
268
+ end
269
+
270
+ def [](name)
271
+ read_attribute(name)
272
+ end
273
+
274
+ def []=(name, value)
275
+ ensure_key_exists(name)
276
+ write_attribute(name, value)
277
+ end
278
+
279
+ def ==(other)
280
+ other.is_a?(self.class) && _id == other._id
281
+ end
282
+
283
+ def id
284
+ read_attribute(:_id)
285
+ end
286
+
287
+ def id=(value)
288
+ if self.class.using_object_id?
289
+ value = MongoMapper.normalize_object_id(value)
290
+ else
291
+ @using_custom_id = true
292
+ end
293
+
294
+ write_attribute :_id, value
295
+ end
296
+
297
+ def using_custom_id?
298
+ !!@using_custom_id
299
+ end
300
+
301
+ def inspect
302
+ attributes_as_nice_string = key_names.collect do |name|
303
+ "#{name}: #{read_attribute(name).inspect}"
304
+ end.join(", ")
305
+ "#<#{self.class} #{attributes_as_nice_string}>"
306
+ end
307
+
308
+ def save
309
+ if _root_document
310
+ _root_document.save
311
+ end
312
+ end
313
+
314
+ def save!
315
+ if _root_document
316
+ _root_document.save!
317
+ end
318
+ end
319
+
320
+ def update_attributes(attrs={})
321
+ self.attributes = attrs
322
+ save
323
+ end
324
+
325
+ def update_attributes!(attrs={})
326
+ self.attributes = attrs
327
+ save!
328
+ end
329
+
330
+ def logger
331
+ self.class.logger
332
+ end
333
+
334
+ private
335
+ def _keys
336
+ self.metaclass.keys
337
+ end
338
+
339
+ def key_names
340
+ _keys.keys
341
+ end
342
+
343
+ def non_embedded_keys
344
+ _keys.values.select { |key| !key.embeddable? }
345
+ end
346
+
347
+ def embedded_keys
348
+ _keys.values.select { |key| key.embeddable? }
349
+ end
350
+
351
+ def ensure_key_exists(name)
352
+ self.metaclass.key(name) unless respond_to?("#{name}=")
353
+ end
354
+
355
+ def read_attribute(name)
356
+ if key = _keys[name]
357
+ value = key.get(instance_variable_get("@#{name}"))
358
+ instance_variable_set "@#{name}", value if !frozen?
359
+ value
360
+ else
361
+ raise KeyNotFound, "Could not find key: #{name.inspect}"
362
+ end
363
+ end
364
+
365
+ def read_attribute_before_typecast(name)
366
+ instance_variable_get("@#{name}_before_typecast")
367
+ end
368
+
369
+ def write_attribute(name, value)
370
+ key = _keys[name]
371
+ instance_variable_set "@#{name}_before_typecast", value
372
+ instance_variable_set "@#{name}", key.set(value)
373
+ end
374
+
375
+ def embedded_associations
376
+ associations.select do |name, association|
377
+ association.embeddable?
378
+ end.map do |name, association|
379
+ association
380
+ end
381
+ end
382
+ end # InstanceMethods
383
+ end # EmbeddedDocument
384
+ end # MongoMapper
@@ -0,0 +1,133 @@
1
+ module MongoMapper
2
+ # Controls the parsing and handling of options used by finders.
3
+ #
4
+ # == Important Note
5
+ #
6
+ # This class is private to MongoMapper and should not be considered part of
7
+ # MongoMapper's public API. Some documentation herein, however, may prove
8
+ # useful for understanding how MongoMapper handles the parsing of finder
9
+ # conditions and options.
10
+ #
11
+ # @private
12
+ class FinderOptions
13
+ OptionKeys = [:fields, :select, :skip, :offset, :limit, :sort, :order]
14
+
15
+ def initialize(model, options)
16
+ raise ArgumentError, "Options must be a hash" unless options.is_a?(Hash)
17
+ options = options.symbolize_keys
18
+
19
+ @model = model
20
+ @options = {}
21
+ @conditions = options.delete(:conditions) || {}
22
+
23
+ options.each_pair do |key, value|
24
+ if OptionKeys.include?(key)
25
+ @options[key] = value
26
+ else
27
+ @conditions[key] = value
28
+ end
29
+ end
30
+
31
+ add_sci_scope
32
+ end
33
+
34
+ # @return [Hash] Mongo compatible criteria options
35
+ #
36
+ # @see FinderOptions#to_mongo_criteria
37
+ def criteria
38
+ to_mongo_criteria(@conditions)
39
+ end
40
+
41
+ # @return [Hash] Mongo compatible options
42
+ def options
43
+ fields = @options.delete(:fields) || @options.delete(:select)
44
+ skip = @options.delete(:skip) || @options.delete(:offset) || 0
45
+ limit = @options.delete(:limit) || 0
46
+ sort = @options.delete(:sort) || convert_order_to_sort(@options.delete(:order))
47
+
48
+ {:fields => to_mongo_fields(fields), :skip => skip.to_i, :limit => limit.to_i, :sort => sort}
49
+ end
50
+
51
+ # @return [Array<Hash>] Mongo criteria and options enclosed in an Array
52
+ def to_a
53
+ [criteria, options]
54
+ end
55
+
56
+ private
57
+ def to_mongo_criteria(conditions, parent_key=nil)
58
+ criteria = {}
59
+
60
+ conditions.each_pair do |field, value|
61
+ field = normalized_field(field)
62
+
63
+ if @model.object_id_key?(field) && value.is_a?(String)
64
+ value = Mongo::ObjectID.from_string(value)
65
+ end
66
+
67
+ if field.is_a?(FinderOperator)
68
+ criteria.merge!(field.to_criteria(value))
69
+ next
70
+ end
71
+
72
+ case value
73
+ when Array
74
+ criteria[field] = operator?(field) ? value : {'$in' => value}
75
+ when Hash
76
+ criteria[field] = to_mongo_criteria(value, field)
77
+ else
78
+ criteria[field] = value
79
+ end
80
+ end
81
+
82
+ criteria
83
+ end
84
+
85
+ def operator?(field)
86
+ field.to_s =~ /^\$/
87
+ end
88
+
89
+ def normalized_field(field)
90
+ field.to_s == 'id' ? :_id : field
91
+ end
92
+
93
+ # adds _type single collection inheritance scope for models that need it
94
+ def add_sci_scope
95
+ if @model.single_collection_inherited?
96
+ @conditions[:_type] = @model.to_s
97
+ end
98
+ end
99
+
100
+ def to_mongo_fields(fields)
101
+ return if fields.blank?
102
+
103
+ if fields.is_a?(String)
104
+ fields.split(',').map { |field| field.strip }
105
+ else
106
+ fields.flatten.compact
107
+ end
108
+ end
109
+
110
+ def convert_order_to_sort(sort)
111
+ return if sort.blank?
112
+ pieces = sort.split(',')
113
+ pieces.map { |s| to_mongo_sort_piece(s) }
114
+ end
115
+
116
+ def to_mongo_sort_piece(str)
117
+ field, direction = str.strip.split(' ')
118
+ direction ||= 'ASC'
119
+ direction = direction.upcase == 'ASC' ? 1 : -1
120
+ [field, direction]
121
+ end
122
+ end
123
+
124
+ class FinderOperator
125
+ def initialize(field, operator)
126
+ @field, @operator = field, operator
127
+ end
128
+
129
+ def to_criteria(value)
130
+ {@field => {@operator => value}}
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,36 @@
1
+ module MongoMapper
2
+ class Key
3
+ attr_accessor :name, :type, :options, :default_value
4
+
5
+ def initialize(*args)
6
+ options = args.extract_options!
7
+ @name, @type = args.shift.to_s, args.shift
8
+ self.options = (options || {}).symbolize_keys
9
+ self.default_value = self.options.delete(:default)
10
+ end
11
+
12
+ def ==(other)
13
+ @name == other.name && @type == other.type
14
+ end
15
+
16
+ def set(value)
17
+ type.to_mongo(value)
18
+ end
19
+
20
+ def embeddable?
21
+ type.respond_to?(:embeddable?) && type.embeddable? ? true : false
22
+ end
23
+
24
+ def number?
25
+ [Integer, Float].include?(type)
26
+ end
27
+
28
+ def get(value)
29
+ if value.nil? && !default_value.nil?
30
+ return default_value
31
+ end
32
+
33
+ type.from_mongo(value)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,50 @@
1
+ require 'observer'
2
+ require 'singleton'
3
+ require 'set'
4
+
5
+ module MongoMapper
6
+ module Observing #:nodoc:
7
+ def self.included(model)
8
+ model.class_eval do
9
+ extend Observable
10
+ end
11
+ end
12
+ end
13
+
14
+ class Observer
15
+ include Singleton
16
+
17
+ class << self
18
+ def observe(*models)
19
+ models.flatten!
20
+ models.collect! { |model| model.is_a?(Symbol) ? model.to_s.camelize.constantize : model }
21
+ define_method(:observed_classes) { Set.new(models) }
22
+ end
23
+
24
+ def observed_class
25
+ if observed_class_name = name[/(.*)Observer/, 1]
26
+ observed_class_name.constantize
27
+ else
28
+ nil
29
+ end
30
+ end
31
+ end
32
+
33
+ def initialize
34
+ Set.new(observed_classes).each { |klass| add_observer! klass }
35
+ end
36
+
37
+ def update(observed_method, object) #:nodoc:
38
+ send(observed_method, object) if respond_to?(observed_method)
39
+ end
40
+
41
+ protected
42
+ def observed_classes
43
+ Set.new([self.class.observed_class].compact.flatten)
44
+ end
45
+
46
+ def add_observer!(klass)
47
+ klass.add_observer(self)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,55 @@
1
+ module MongoMapper
2
+ module Pagination
3
+ class PaginationProxy
4
+ instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|proxy_|^object_id$)/ }
5
+
6
+ attr_accessor :subject
7
+ attr_reader :total_entries, :per_page, :current_page
8
+ alias limit per_page
9
+
10
+ def initialize(total_entries, current_page, per_page=nil)
11
+ @total_entries = total_entries.to_i
12
+ self.per_page = per_page
13
+ self.current_page = current_page
14
+ end
15
+
16
+ def total_pages
17
+ (total_entries / per_page.to_f).ceil
18
+ end
19
+
20
+ def out_of_bounds?
21
+ current_page > total_pages
22
+ end
23
+
24
+ def previous_page
25
+ current_page > 1 ? (current_page - 1) : nil
26
+ end
27
+
28
+ def next_page
29
+ current_page < total_pages ? (current_page + 1) : nil
30
+ end
31
+
32
+ def skip
33
+ (current_page - 1) * per_page
34
+ end
35
+ alias offset skip # for will paginate support
36
+
37
+
38
+ def method_missing(name, *args, &block)
39
+ @subject.send(name, *args, &block)
40
+ end
41
+
42
+ private
43
+ def per_page=(value)
44
+ value = 25 if value.blank?
45
+ @per_page = value.to_i
46
+ end
47
+
48
+ def current_page=(value)
49
+ value = value.to_i
50
+ value = 1 if value < 1
51
+ @current_page = value
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,15 @@
1
+ module MongoMapper
2
+ module RailsCompatibility
3
+ module Document
4
+ def self.included(model)
5
+ model.class_eval do
6
+ alias_method :new_record?, :new?
7
+
8
+ def human_name
9
+ self.name.demodulize.titleize
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,27 @@
1
+ module MongoMapper
2
+ module RailsCompatibility
3
+ module EmbeddedDocument
4
+ def self.included(model)
5
+ model.class_eval do
6
+ extend ClassMethods
7
+
8
+ alias_method :new_record?, :new?
9
+ end
10
+
11
+ class << model
12
+ alias has_many many
13
+ end
14
+ end
15
+
16
+ module ClassMethods
17
+ def column_names
18
+ keys.keys
19
+ end
20
+
21
+ def human_name
22
+ self.name.demodulize.titleize
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end