simply_couch 0.1.0

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 (45) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +182 -0
  3. data/LICENSE.txt +15 -0
  4. data/README.md +294 -0
  5. data/lib/core_ext/date.rb +15 -0
  6. data/lib/core_ext/time.rb +23 -0
  7. data/lib/simply_couch/class_methods_base.rb +72 -0
  8. data/lib/simply_couch/has_attachment.rb +225 -0
  9. data/lib/simply_couch/include_relation.rb +160 -0
  10. data/lib/simply_couch/instance_methods.rb +356 -0
  11. data/lib/simply_couch/locale/en.yml +5 -0
  12. data/lib/simply_couch/model/ancestry.rb +307 -0
  13. data/lib/simply_couch/model/association_property.rb +26 -0
  14. data/lib/simply_couch/model/attachments.rb +90 -0
  15. data/lib/simply_couch/model/belongs_to.rb +140 -0
  16. data/lib/simply_couch/model/database.rb +209 -0
  17. data/lib/simply_couch/model/embedded_in.rb +196 -0
  18. data/lib/simply_couch/model/find_by.rb +202 -0
  19. data/lib/simply_couch/model/finders.rb +77 -0
  20. data/lib/simply_couch/model/has_and_belongs_to_many.rb +223 -0
  21. data/lib/simply_couch/model/has_many.rb +177 -0
  22. data/lib/simply_couch/model/has_many_embedded.rb +187 -0
  23. data/lib/simply_couch/model/has_one.rb +75 -0
  24. data/lib/simply_couch/model/pagination.rb +25 -0
  25. data/lib/simply_couch/model/pagination_options.rb +55 -0
  26. data/lib/simply_couch/model/persistence.rb +411 -0
  27. data/lib/simply_couch/model/properties.rb +11 -0
  28. data/lib/simply_couch/model/validations.rb +28 -0
  29. data/lib/simply_couch/model/view/base_view_spec.rb +115 -0
  30. data/lib/simply_couch/model/view/custom_view_spec.rb +49 -0
  31. data/lib/simply_couch/model/view/custom_views.rb +50 -0
  32. data/lib/simply_couch/model/view/lists.rb +25 -0
  33. data/lib/simply_couch/model/view/model_view_spec.rb +106 -0
  34. data/lib/simply_couch/model/view/properties_view_spec.rb +53 -0
  35. data/lib/simply_couch/model/view/raw_view_spec.rb +30 -0
  36. data/lib/simply_couch/model/view/view_query.rb +98 -0
  37. data/lib/simply_couch/model/view.rb +8 -0
  38. data/lib/simply_couch/model/views/array_property_view_spec.rb +26 -0
  39. data/lib/simply_couch/model/views/deleted_model_view_spec.rb +43 -0
  40. data/lib/simply_couch/model/views.rb +2 -0
  41. data/lib/simply_couch/model.rb +195 -0
  42. data/lib/simply_couch/rake.rb +23 -0
  43. data/lib/simply_couch/storage.rb +147 -0
  44. data/lib/simply_couch.rb +26 -0
  45. metadata +144 -0
@@ -0,0 +1,356 @@
1
+ module SimplyCouch
2
+ module InstanceMethods
3
+ def initialize(attributes = {}, &blk)
4
+ super(_remove_protected_attributes(attributes))
5
+ blk.call(self) if blk
6
+ end
7
+ def ==(other)
8
+ other.kind_of?(SimplyCouch::Model) && other._id == _id && other._rev == _rev
9
+ end
10
+
11
+ def eql?(other)
12
+ self.==(other)
13
+ end
14
+
15
+ def save(validate = true)
16
+ validate = validate[:validate] if validate.is_a?(Hash) && validate.has_key?(:validate)
17
+ result = retry_on_conflict do
18
+ retry_on_connection_error do
19
+ self.class.database.save_document(self, validate)
20
+ end
21
+ end
22
+ # ActiveModel implementations
23
+ @previously_changed = changes
24
+ #@changed_attributes.clear
25
+ return result
26
+ end
27
+
28
+ def save!
29
+ result = retry_on_conflict do
30
+ retry_on_connection_error do
31
+ self.class.database.save_document!(self)
32
+ end
33
+ end
34
+ # ActiveModel implementations
35
+ @previously_changed = changes
36
+ #@changed_attributes.clear
37
+ return result
38
+ end
39
+
40
+ def destroy(override_soft_delete=false)
41
+ check_and_destroy_dependents
42
+ if self.class.soft_deleting_enabled? && !override_soft_delete
43
+ # soft-delete
44
+ _mark_as_deleted
45
+ else
46
+ if self.class.soft_deleting_enabled? && deleted?
47
+ # really deleting a previously soft-deleted object - skipping callbacks
48
+ self.class.database.destroy_document(self, false)
49
+ else # deleting a normal object or a soft-deletable object that was not soft-deleted before
50
+ self.class.database.destroy_document(self, true)
51
+ end
52
+ freeze
53
+ end
54
+ self
55
+ end
56
+ alias :delete :destroy
57
+
58
+ def update_attributes(attributes = {})
59
+ parent_id_present = attributes.delete(:parent_id) || attributes.delete('parent_id')
60
+ self.attributes = attributes
61
+ if parent_id_present
62
+ if valid?
63
+ self.parent_id = parent_id_present
64
+ save # only for return value, revalidate etc.
65
+ else
66
+ return false
67
+ end
68
+ else
69
+ save
70
+ end
71
+ end
72
+ alias_method :update, :update_attributes
73
+
74
+ def attributes=(attr)
75
+ super(_remove_protected_attributes(attr))
76
+ end
77
+
78
+ def reload
79
+ @children = nil
80
+ @descendants = nil
81
+ instance = self.class.find(_id, :with_deleted => true)
82
+ instance.attributes.each do |attribute, value|
83
+ send "#{attribute}=", value
84
+ end
85
+ self._rev = instance._rev
86
+ self._attachments = instance._attachments # CouchDB internal, not a property
87
+ clear_changes_information
88
+ reset_association_caches
89
+ self
90
+ end
91
+
92
+ def deleted?
93
+ if self.class.soft_deleting_enabled?
94
+ !send(self.class.soft_delete_attribute).nil?
95
+ else
96
+ false
97
+ end
98
+ end
99
+
100
+ def serializable_hash(*)
101
+ {'id' => self.id}.merge attributes
102
+ end
103
+
104
+ protected
105
+
106
+ def retry_on_connection_error(max_retries = 2, &blk)
107
+ retry_count = 0
108
+ begin
109
+ blk.call
110
+ rescue Errno::ECONNREFUSED => e
111
+ if retry_count < max_retries
112
+ retry_count += 1
113
+ retry
114
+ else
115
+ raise e
116
+ end
117
+ end
118
+ end
119
+
120
+ def retry_on_conflict(max_retries = 2, &blk)
121
+ retry_count = 0
122
+ begin
123
+ _reset_conflict_information
124
+ blk.call
125
+ rescue SimplyCouch::Conflict => e
126
+ if self.class.auto_conflict_resolution_on_save && retry_count < max_retries && try_to_merge_conflict
127
+ retry_count += 1
128
+ retry
129
+ else
130
+ _decorate_with_conflict_details(e) if e.is_a?(SimplyCouch::Conflict)
131
+ raise e
132
+ end
133
+ end
134
+ end
135
+
136
+ def _decorate_with_conflict_details(exception)
137
+ if @_conflict_information.present?
138
+ def exception.metaclass
139
+ class << self
140
+ self
141
+ end
142
+ end
143
+ local_conflict_information = @_conflict_information
144
+ exception.metaclass.send(:define_method, :message){ "409 Conflict - conflict on attributes: #{local_conflict_information.inspect}" }
145
+ end
146
+ end
147
+
148
+ def try_to_merge_conflict
149
+ original = self.class.find(id)
150
+ our_attributes = self.attributes.dup
151
+ their_attributes = original.attributes.dup
152
+ _clear_non_relevant_attributes(our_attributes)
153
+ _clear_non_relevant_attributes(their_attributes)
154
+ if _merge_possible?(our_attributes, their_attributes)
155
+ _copy_non_conflicting_attributes(our_attributes, their_attributes)
156
+ self._rev = original._rev
157
+ @_document['_rev'] = original._rev if @_document # keep CouchDB cache in sync
158
+ true
159
+ else
160
+ @_conflict_information = _conflicting_attributes(our_attributes, their_attributes)
161
+ false
162
+ end
163
+ end
164
+
165
+ def _reset_conflict_information
166
+ @_conflict_information = nil
167
+ end
168
+
169
+ def _clear_non_relevant_attributes(attr_list)
170
+ [:updated_at, :created_at, :id, :rev, :_id, :_rev].each do |skipped_attribute|
171
+ attr_list.delete(skipped_attribute)
172
+ end
173
+ end
174
+
175
+ def _copy_non_conflicting_attributes(our_attributes, their_attributes)
176
+ their_attributes.each do |attr_name, their_value|
177
+ if !self.public_send("#{attr_name}_changed?") && our_attributes[attr_name] != their_value
178
+ self.public_send("#{attr_name}=", their_value)
179
+ end
180
+ end
181
+ end
182
+
183
+ def _merge_possible?(our_attributes, their_attributes)
184
+ _conflicting_attributes(our_attributes, their_attributes).empty?
185
+ end
186
+
187
+ def _conflicting_attributes(our_attributes, their_attributes)
188
+ their_attributes.keys.delete_if do |attr_name|
189
+ _attribute_not_in_conflict?(attr_name, our_attributes[attr_name], their_attributes[attr_name])
190
+ end
191
+ end
192
+
193
+ def _attribute_not_in_conflict?(attr_name, our_value, their_value)
194
+ our_value == their_value || # same
195
+ !self.public_send("#{attr_name}_changed?") || # we didn't change
196
+ self.public_send("#{attr_name}_changed?") && their_value == self.public_send("#{attr_name}_was") # we changed and they kept the original
197
+ end
198
+
199
+ def reset_association_caches
200
+ self.class.properties.each do |property|
201
+ if property.respond_to?(:association?) && property.association?
202
+ instance_variable_set("@#{property.name}", nil)
203
+ end
204
+ end
205
+ end
206
+
207
+ def _remove_protected_attributes(attrs)
208
+ return {} if attrs.blank?
209
+ attrs = attrs.dup.stringify_keys
210
+ (self.class.instance_variable_get(:@_protected_attributes) || []).map(&:to_s).each do |protected_attribute|
211
+ attrs.delete(protected_attribute)
212
+ end
213
+
214
+ accessible_attributes = (self.class.instance_variable_get(:@_accessible_attributes) || []).map(&:to_s)
215
+
216
+ if accessible_attributes.present?
217
+ attrs.each do |attr_key, attr_value|
218
+ attrs.delete(attr_key) unless accessible_attributes.include?(attr_key)
219
+ end
220
+ end
221
+
222
+ attrs
223
+ end
224
+
225
+ def check_and_destroy_dependents
226
+ self.class.properties.each do |property|
227
+ if property.respond_to?(:association?) and property.association?
228
+ if property.is_a?(SimplyCouch::Model::HasAndBelongsToMany::Property)
229
+ has_and_belongs_to_many_clean_up_after_destroy(property)
230
+ else
231
+ next unless property.options[:dependent]
232
+ next if property.options[:through]
233
+ dependents = send(property.name, :force_reload => true)
234
+ dependents = [dependents] unless dependents.is_a?(Array)
235
+ dependents.reject{|d| d.nil?}.each do |dependent|
236
+ case property.options[:dependent]
237
+ when :destroy
238
+ dependent.destroy
239
+ when :ignore
240
+ # skip
241
+ else
242
+ # nullify
243
+ unless dependent.class.soft_deleting_enabled? && dependent.deleted?
244
+ foreign_property = self.class.foreign_property
245
+ foreign_property_without_namespace = foreign_property.to_s.split('__').last
246
+ if dependent.respond_to?("#{foreign_property}=")
247
+ dependent.public_send("#{foreign_property}=", nil)
248
+ elsif dependent.respond_to?("#{foreign_property_without_namespace}=")
249
+ dependent.public_send("#{foreign_property_without_namespace}=", nil)
250
+ end
251
+ dependent.save(false)
252
+ end
253
+ end
254
+ end
255
+ end
256
+ end
257
+ end
258
+ end
259
+
260
+ def _default_view_options(options = {})
261
+ view_options = {:include_docs => true, :reduce => false}
262
+ view_options[:descending] = options[:descending] if options[:descending]
263
+ if view_options[:descending]
264
+ view_options[:startkey] = ["#{id}\u9999"]
265
+ view_options[:endkey] = [id]
266
+ else
267
+ view_options[:startkey] = [id]
268
+ view_options[:endkey] = ["#{id}\u9999"]
269
+ end
270
+ view_options[:limit] = options[:limit] if options[:limit]
271
+ view_options
272
+ end
273
+
274
+ def find_one_associated(from, to, options = {})
275
+ options = {
276
+ :limit => 1,
277
+ :descending => true
278
+ }.update(options)
279
+ find_associated(from, to, options).first
280
+ end
281
+
282
+ def find_associated(from, to, options = {})
283
+ foreign_key = (options.delete(:foreign_key) || self.class.name.singularize.property_name.foreign_key).to_s.gsub(/_id$/, '')
284
+ view_options = _default_view_options(options)
285
+
286
+ if options[:with_deleted]
287
+ self.class.database.view(
288
+ self.class.get_class_from_name(from).public_send(
289
+ "association_#{from.to_s.singularize.property_name}_belongs_to_#{foreign_key}_with_deleted", view_options))
290
+ else
291
+ self.class.database.view(
292
+ self.class.get_class_from_name(from).public_send(
293
+ "association_#{from.to_s.singularize.property_name}_belongs_to_#{foreign_key}", view_options))
294
+ end
295
+ end
296
+
297
+ def get_embedded(*args)
298
+ puts args.inspect
299
+ []
300
+ end
301
+
302
+ def count_associated(from, to, options = {})
303
+ view_options = _default_view_options(options)
304
+ view_options[:reduce] = true
305
+ view_options[:include_docs] = false
306
+
307
+ if options[:with_deleted]
308
+ self.class.database.view(
309
+ self.class.get_class_from_name(from).public_send(
310
+ "association_#{from.to_s.singularize.property_name}_belongs_to_#{to.name.singularize.property_name}_with_deleted", view_options))
311
+ else
312
+ self.class.database.view(
313
+ self.class.get_class_from_name(from).public_send(
314
+ "association_#{from.to_s.singularize.property_name}_belongs_to_#{to.name.singularize.property_name}", view_options))
315
+ end
316
+ end
317
+
318
+ def find_associated_via_join_view(from, to, options = {})
319
+ foreign_key = options.delete(:foreign_key).to_s.gsub(/_ids$/, '').pluralize
320
+ view_options = _default_view_options(options)
321
+
322
+ if options[:with_deleted]
323
+ self.class.database.view(
324
+ self.class.get_class_from_name(from).public_send(
325
+ "association_#{from.to_s.singularize.property_name}_has_and_belongs_to_many_#{to.name.pluralize.property_name}_with_deleted", view_options))
326
+ else
327
+ self.class.database.view(
328
+ self.class.get_class_from_name(from).public_send(
329
+ "association_#{from.to_s.singularize.property_name}_has_and_belongs_to_many_#{to.name.pluralize.property_name}", view_options))
330
+ end
331
+ end
332
+
333
+ def count_associated_via_join_view(from, to, options = {})
334
+ view_options = _default_view_options(options)
335
+ view_options[:reduce] = true
336
+ view_options[:include_docs] = false
337
+
338
+ if options[:with_deleted]
339
+ self.class.database.view(
340
+ self.class.get_class_from_name(from).public_send(
341
+ "association_#{from.to_s.singularize.property_name}_has_and_belongs_to_many_#{to.name.pluralize.property_name}_with_deleted", view_options))
342
+ else
343
+ self.class.database.view(
344
+ self.class.get_class_from_name(from).public_send(
345
+ "association_#{from.to_s.singularize.property_name}_has_and_belongs_to_many_#{to.name.pluralize.property_name}", view_options))
346
+ end
347
+ end
348
+
349
+ def _mark_as_deleted
350
+ run_callbacks :destroy do
351
+ send("#{self.class.soft_delete_attribute}=", Time.now)
352
+ save(false)
353
+ end
354
+ end
355
+ end
356
+ end
@@ -0,0 +1,5 @@
1
+ en:
2
+ errors:
3
+ messages:
4
+ taken: is already taken
5
+ no_parent: should be parent object
@@ -0,0 +1,307 @@
1
+ module SimplyCouch
2
+ module Model
3
+ module Ancestry
4
+ module InstanceMethods
5
+ def children
6
+ return @children if @children
7
+ if root_property = self.class.ancestry_by_property
8
+ @children = self.class.database.view(self.class.children_view(startkey: [send(root_property), id], endkey: [send(root_property), id, {}], reduce: false))
9
+ else
10
+ @children = self.class.database.view(self.class.children_view(startkey: [id], endkey: [id, {}], reduce: false))
11
+ end
12
+ @children
13
+ end
14
+ def children=(val)
15
+ @old_descendants = descendants
16
+ @children = val
17
+ @children.map(&:subtree) # preload children hierarchy
18
+ @new_descendants = []
19
+ for child in @children
20
+ for descendant in child.descendants
21
+ @new_descendants << descendant
22
+ end
23
+ end
24
+ @old_descendants.each{|d| d.instance_variable_set('@parent', nil); d.path_ids = [d.id]} # old descendants become root
25
+ (@old_descendants - @new_descendants).map(&:save) # Persist old descendants not present in new descendants
26
+
27
+ # update by_property of children if it differs from parent (locale or ... orther field is required to have the same values)
28
+ if root_property = self.class.ancestry_by_property
29
+ self_by_property = send(root_property)
30
+ for child in @children | @new_descendants
31
+ child.send("#{root_property}=", self_by_property) if self_by_property != child.send(root_property)
32
+ end
33
+ end
34
+
35
+ self.class.set_parent(self, @children) # recurring update of parent in subtree of children
36
+ @descendants = (@children | @new_descendants)
37
+ clear_cached_ancestors
38
+
39
+ # Return wether all children can be saved :)
40
+ (@descendants).map(&:save).all?
41
+ end
42
+
43
+ # reset cache attributes for ancestors
44
+ def clear_cached_ancestors
45
+ ansi = self
46
+ while ancestor = ansi.instance_variable_get('@parent').presence
47
+ ancestor.instance_variable_set('@descendants', nil)
48
+ ancestor.instance_variable_set('@children', nil)
49
+ ansi = ancestor
50
+ end
51
+ self
52
+ end
53
+
54
+ def add_child(child)
55
+ unless children.include?(child)
56
+ @children ||= []
57
+ @children += [child]
58
+ child.parent = self
59
+
60
+ # update by_property of child if it differs from parent
61
+ if root_property = self.class.ancestry_by_property
62
+ child.send("#{root_property}=", send(root_property)) if send(root_property) != child.send(root_property)
63
+ end
64
+ @descendants = nil # reload descendants if requested
65
+ end
66
+ child
67
+ end
68
+
69
+ # Get all descendants
70
+ def descendants
71
+ return @descendants if @descendants
72
+ return @descendants = [] if id.blank?
73
+ if root_property = self.class.ancestry_by_property
74
+ @descendants = self.class.database.view(self.class.subtree_view(startkey: [send(root_property), id], endkey: [send(root_property), id, {}], reduce: false)).sort_by!{|d| [d.path_ids.size, d.position]}
75
+ else
76
+ @descendants = self.class.database.view(self.class.subtree_view(startkey: [id], endkey: [id, {}], reduce: false)).sort_by{|d| [d.path_ids.size, d.position]}
77
+ end
78
+ @children = self.class.build_tree(@descendants)
79
+ @descendants
80
+ end
81
+
82
+ # Find subtree of a given page and set children with the result (important to get same children object ids as in descendants)
83
+ def subtree
84
+ @children = self.class.build_tree(descendants)
85
+ end
86
+
87
+ # Triggered before save
88
+ def update_tree_path
89
+ return false unless id
90
+ newpath = ( (parent && parent.path_ids) || []) + [id]
91
+ if path_ids != newpath
92
+ path_ids_will_change!
93
+ self.path_ids = newpath
94
+ end
95
+ self
96
+ end
97
+
98
+ # Triggered after create, because needs id
99
+ def create_tree_path
100
+ return false unless id
101
+ newpath = ( (parent && parent.path_ids) || []) + [id]
102
+ return true if path_ids == newpath
103
+ self.path_ids = newpath
104
+ save
105
+ end
106
+
107
+ def parent
108
+ return @parent if @parent
109
+ return @parent = self.class.find(parent_id) if parent_id.present?
110
+ nil
111
+ end
112
+
113
+ def parent=(val)
114
+ if @parent != val
115
+ @parent = val
116
+ update_tree_path
117
+ @parent.children += [self] unless @parent.children.include?(self)
118
+ save
119
+ end
120
+ val
121
+ end
122
+
123
+ def parent_id=(val)
124
+ if parent_id != val.presence
125
+ @parent = nil
126
+ @parent_id = val.presence
127
+ subtree if persisted?
128
+ path_ids_will_change!
129
+ if @parent_id
130
+ @parent = parent
131
+ self.path_ids = parent.path_ids + [id]
132
+ self.class.set_parent(self, @children) if persisted?
133
+ else
134
+ self.path_ids = [id]
135
+ self.class.set_parent(self, @children) if persisted?
136
+ end
137
+ @descendants.map(&:save) if persisted? && save
138
+ end
139
+ end
140
+
141
+ def parent_id
142
+ @parent_id ||= parent_ids.last
143
+ end
144
+
145
+ def parent_ids
146
+ (path_ids || [])[0..-2]
147
+ end
148
+
149
+ def ancestors
150
+ return [] unless parent_ids.any?
151
+ return [parent] if parent_ids.size == 1 # optimization, parent is pre-loaded many times
152
+ (self.class.database.couchrest_database.bulk_load(parent_ids)['rows'] || []).map{|h| h['doc']}.compact
153
+ end
154
+ def parents
155
+ ancestors
156
+ end
157
+
158
+ def tree_path
159
+ ancestors + [self]
160
+ end
161
+
162
+ def tree_depth
163
+ (path_ids || [nil]).size - 1
164
+ end
165
+ end
166
+
167
+ module TreeBuilder
168
+
169
+ end
170
+
171
+ # Add ancestry logic to your model
172
+ # class Page
173
+ # include SimplyCouch::Model
174
+ # has_ancestry
175
+ # end
176
+ # or
177
+ # class Page
178
+ # include SimplyCouch::Model
179
+ # property :locale
180
+ # has_ancestry by_property: :locale
181
+ # end
182
+ def has_ancestry(options = {})
183
+ @ancestry_by_property = nil
184
+ def self.ancestry_by_property
185
+ @ancestry_by_property
186
+ end
187
+ options = {order_by: :position}.merge(options)
188
+ order_by = case options[:order_by]
189
+ when Symbol then "doc['#{options[:order_by]}']"
190
+ when Array then "[#{options[:order_by].map{|o| "doc['#{o}']"}.join(', ')}]"
191
+ else "doc['position']"
192
+ end
193
+ property :path_ids, type: Array, default: []
194
+ property :position, type: Integer, default: 0
195
+ if options[:by_property].present?
196
+ property options[:by_property] unless property_names.include?(options[:by_property])
197
+ @ancestry_by_property = options[:by_property]
198
+ by_property_view_prefix = options[:by_property].present? ? "doc['#{options[:by_property]}'], " : ''
199
+ end
200
+
201
+ view :subtree_view, type: :custom, include_docs: true, map_function: %|function(doc){
202
+ if(doc['ruby_class'] == '#{name}' && doc.path_ids){
203
+ for(var i = 0; i < doc.path_ids.length - 1; i++){
204
+ emit([#{by_property_view_prefix}doc.path_ids[i], #{order_by}], 1);
205
+ }
206
+ }
207
+ }|, reduce_function: "_sum"
208
+
209
+ view :children_view, type: :custom, include_docs: true, map_function: %|function(doc){
210
+ if(doc['ruby_class'] == '#{name}' && doc.path_ids){
211
+ emit([#{by_property_view_prefix}doc.path_ids.slice(-2,-1)[0], #{order_by}], 1);
212
+ }
213
+ }|, reduce_function: "_sum"
214
+ view :roots_view, conditions: "doc.path_ids && doc.path_ids.length == 1", key: [options[:by_property].presence, options[:order_by]].compact
215
+ include SimplyCouch::Model::Ancestry::InstanceMethods
216
+ extend SimplyCouch::Model::Ancestry::ClassMethods
217
+ before_update :update_tree_path
218
+ after_create :create_tree_path
219
+ end
220
+ module ClassMethods
221
+ def roots(options = {})
222
+ if root_property = ancestry_by_property
223
+ if options.blank?
224
+ options = {}
225
+ elsif options.is_a?(Symbol)
226
+ options = {startkey: [options.to_s], endkey: [options.to_s, {}]}
227
+ elsif options.is_a?(String)
228
+ options = {startkey: [options], endkey: [options, {}]}
229
+ elsif options.keys.include?(root_property)
230
+ root_key = options.delete(root_property)
231
+ options[:startkey] = [root_key.to_s]
232
+ options[:endkey] = [root_key.to_s, {}]
233
+ end
234
+ end
235
+ options[:reduce] = false
236
+ database.view(roots_view(options))
237
+ end
238
+
239
+ def full_tree(options = {})
240
+ if root_property = ancestry_by_property
241
+ if options.blank?
242
+ records = all
243
+ elsif options.is_a?(Array)
244
+ records = options
245
+ elsif (options.is_a?(Symbol) || options.is_a?(String))
246
+ records = send("find_all_by_#{root_property}", options.to_s)
247
+ elsif options.is_a?(Hash) && options.keys.include?(root_property)
248
+ root_key = options.delete(root_property)
249
+ records = send("find_all_by_#{root_property}", root_key.to_s)
250
+ else
251
+ records = options[:records].presence || all
252
+ end
253
+ else
254
+ records = options.is_a?(Array) ? options : (options.present? && options[:records].presence) || all
255
+ end
256
+ build_tree(records) #.first.children
257
+ end
258
+
259
+ # Build a tree from a flat set of pages making use of the path attribute
260
+ def build_tree(pages = nil)
261
+ pages ||= all
262
+ return pages if pages.empty? # Do not process empty array
263
+ @tree_wrapper = Struct.new(:children).new([]) # Dummy container as traversing begin, contains roots as children
264
+ old_tree_slice = @tree_wrapper.children
265
+ new_tree_slice = []
266
+ pages.sort_by!{|p| [p.path_ids.size, p.position]}
267
+ root_depth = pages.first.path_ids.size
268
+ current_depth = root_depth + 1 # Start counting/swapping from one depth deeper than first one
269
+ for page in pages
270
+ old_tree_slice << page and next if page.path_ids.size == root_depth # fill first slice with roots
271
+
272
+ # Move further if new depth is reached
273
+ if page.path_ids.size > current_depth
274
+ old_tree_slice = new_tree_slice
275
+ new_tree_slice = []
276
+ current_depth = page.path_ids.size
277
+ end
278
+ parent = old_tree_slice.find{|r| r.id == page.path_ids[-2]} # path id before last is parent id
279
+ next unless parent # page is not associated in tree
280
+
281
+ # Initialize children if needed
282
+ parent.instance_variable_set('@children', []) unless parent.instance_variable_get('@children').is_a?(Array)
283
+
284
+ # Avoid database call on deepest children
285
+ page.instance_variable_set('@children', []) unless page.instance_variable_get('@children').is_a?(Array)
286
+ parent.instance_variable_get('@children') << page
287
+ page.instance_variable_set('@parent', parent)
288
+ page.instance_variable_set('@parent_id', parent.id)
289
+ new_tree_slice << page
290
+ end
291
+
292
+ #TODO: setting @descendants from cache as option to avoid database call when descendants is required
293
+ @tree_wrapper.children
294
+ end
295
+
296
+ # Recursive set parents
297
+ def set_parent(parent, children)
298
+ for child in children.sort_by!(&:position)
299
+ child.instance_variable_set('@parent', parent)
300
+ child.update_tree_path
301
+ set_parent child, child.children
302
+ end
303
+ end
304
+ end
305
+ end
306
+ end
307
+ end
@@ -0,0 +1,26 @@
1
+ module SimplyCouch
2
+ module Model
3
+ class AssociationProperty
4
+ attr_reader :name, :options
5
+
6
+ def dirty?(object)
7
+ false
8
+ end
9
+
10
+ def build(object, json)
11
+ end
12
+
13
+ def serialize(json, object)
14
+ end
15
+ alias :value :serialize
16
+
17
+ def supports_dirty?
18
+ false
19
+ end
20
+
21
+ def association?
22
+ true
23
+ end
24
+ end
25
+ end
26
+ end