mm-referenced-tree 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 (5) hide show
  1. data/LICENSE +20 -0
  2. data/README.rdoc +36 -0
  3. data/Rakefile +74 -0
  4. data/lib/mm-referenced-tree.rb +300 -0
  5. metadata +84 -0
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Richard Livsey
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,36 @@
1
+ = MongoMapper::Plugins::ReferencedTree
2
+
3
+ Yet another tree plugin for MongoMapper, uses an array of reference numbers.
4
+ Useful when the leaf/branch numbers are meaningful, so you don't have to separately maintain them.
5
+
6
+ == Usage
7
+
8
+ Load it into a model:
9
+
10
+ plugin MongoMapper::Plugins::ReferencedTree
11
+
12
+ Then call referenced_tree to configure it
13
+
14
+ referenced_tree :scope => :account_id
15
+
16
+ == Options
17
+
18
+ Available options are:
19
+
20
+ * :scope - scope to a specific field (default - nil)
21
+
22
+ == Note on Patches/Pull Requests
23
+
24
+ * Fork the project.
25
+ * Make your feature addition or bug fix.
26
+ * Add tests for it. This is important so I don't break it in a future version unintentionally.
27
+ * Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself in another branch so I can ignore when I pull)
28
+ * Send me a pull request. Bonus points for topic branches.
29
+
30
+ == Install
31
+
32
+ $ gem install mm-referenced-tree
33
+
34
+ == Copyright
35
+
36
+ See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,74 @@
1
+ require "rubygems"
2
+ require "rake/gempackagetask"
3
+ require "rake/rdoctask"
4
+
5
+ require "spec"
6
+ require "spec/rake/spectask"
7
+ Spec::Rake::SpecTask.new do |t|
8
+ t.spec_opts = %w(--format specdoc --colour)
9
+ t.libs = ["spec"]
10
+ end
11
+
12
+ task :default => ["spec"]
13
+
14
+ # This builds the actual gem. For details of what all these options
15
+ # mean, and other ones you can add, check the documentation here:
16
+ #
17
+ # http://rubygems.org/read/chapter/20
18
+ #
19
+ spec = Gem::Specification.new do |s|
20
+
21
+ # Change these as appropriate
22
+ s.name = "mm-referenced-tree"
23
+ s.version = "0.1.0"
24
+ s.summary = "Yet another tree plugin for MongoMapper, built with an array of reference numbers"
25
+ s.author = "Richard Livsey"
26
+ s.email = "richard@livsey.org"
27
+ s.homepage = "http://github.com/rlivsey/mm-referenced-tree"
28
+
29
+ s.has_rdoc = true
30
+ s.extra_rdoc_files = %w(README.rdoc)
31
+ s.rdoc_options = %w(--main README.rdoc)
32
+
33
+ # Add any extra files to include in the gem
34
+ s.files = %w(LICENSE Rakefile README.rdoc) + Dir.glob("{spec,lib/**/*}")
35
+ s.require_paths = ["lib"]
36
+
37
+ # If you want to depend on other gems, add them here, along with any
38
+ # relevant versions
39
+ # s.add_dependency("some_other_gem", "~> 0.1.0")
40
+
41
+ # If your tests use any gems, include them here
42
+ s.add_development_dependency("rspec")
43
+ end
44
+
45
+ # This task actually builds the gem. We also regenerate a static
46
+ # .gemspec file, which is useful if something (i.e. GitHub) will
47
+ # be automatically building a gem for this project. If you're not
48
+ # using GitHub, edit as appropriate.
49
+ #
50
+ # To publish your gem online, install the 'gemcutter' gem; Read more
51
+ # about that here: http://gemcutter.org/pages/gem_docs
52
+ Rake::GemPackageTask.new(spec) do |pkg|
53
+ pkg.gem_spec = spec
54
+ end
55
+
56
+ desc "Build the gemspec file #{spec.name}.gemspec"
57
+ task :gemspec do
58
+ file = File.dirname(__FILE__) + "/#{spec.name}.gemspec"
59
+ File.open(file, "w") {|f| f << spec.to_ruby }
60
+ end
61
+
62
+ task :package => :gemspec
63
+
64
+ # Generate documentation
65
+ Rake::RDocTask.new do |rd|
66
+ rd.main = "README.rdoc"
67
+ rd.rdoc_files.include("README.rdoc", "lib/**/*.rb")
68
+ rd.rdoc_dir = "rdoc"
69
+ end
70
+
71
+ desc 'Clear out RDoc and generated packages'
72
+ task :clean => [:clobber_rdoc, :clobber_package] do
73
+ rm "#{spec.name}.gemspec"
74
+ end
@@ -0,0 +1,300 @@
1
+ require 'mongo_mapper'
2
+
3
+ module MongoMapper
4
+ module Plugins
5
+ module ReferencedTree
6
+
7
+ module ClassMethods
8
+ def referenced_tree(options={})
9
+ options.reverse_merge!({
10
+ :scope => nil
11
+ })
12
+
13
+ write_inheritable_attribute :referenced_tree_options, options
14
+ class_inheritable_reader :referenced_tree_options
15
+
16
+ key :reference, Array
17
+ key :depth, Integer
18
+
19
+ before_create :assign_reference
20
+ before_create :reposition_subsequent_nodes
21
+ after_destroy :delete_descendants_and_renumber_siblings
22
+ before_update :renumber_tree_if_reference_changed
23
+ end
24
+
25
+ # renumber a full set of nodes
26
+ # pass the scope into the query to limit it to the nodes you want
27
+ # Eg. Something.renumber_tree(:account_id => 123)
28
+ # TODO - make this work on associations, IE account.nodes.renumber_tree
29
+ def renumber_tree(query={})
30
+ reference = [0]
31
+ level = 1
32
+ level_offset = 0
33
+
34
+ where(query).sort(:reference.asc).all.each do |node|
35
+
36
+ # it's a level up
37
+ if node.reference.size > (level + level_offset)
38
+ if reference == [0]
39
+ level_offset = 1
40
+ else
41
+ level += 1
42
+ reference[level-1] = 0
43
+ end
44
+
45
+ # back down a level or more
46
+ elsif node.reference.size < (level + level_offset)
47
+ level = node.depth
48
+
49
+ if level_offset > 0
50
+
51
+ if level == 1
52
+ level_offset = 0
53
+ else
54
+ level -= level_offset
55
+ end
56
+ end
57
+
58
+ reference = reference[0, level]
59
+ end
60
+
61
+ reference[level-1] += 1
62
+
63
+ if node.reference != reference
64
+ node.set(:reference => reference)
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ module InstanceMethods
71
+
72
+ # removes this node and renumbers siblings and descendants
73
+ def destroy
74
+ super
75
+ end
76
+
77
+ # removes this node and all descendants, renumbers siblings
78
+ def destroy_with_children
79
+ @destroy_descendants = true
80
+ destroy
81
+ end
82
+
83
+ # Provides a formatted version of the reference
84
+ #
85
+ # [1,2,3] => "1.2.3"
86
+ #
87
+ # override this in your model if you want to format the references differently
88
+ def formatted_reference
89
+ reference.join(".")
90
+ end
91
+
92
+ def reference=(ref)
93
+ self.depth = ref.size
94
+ super
95
+ end
96
+
97
+ # set the reference without calling #save, so no callbacks are triggered
98
+ # good for renumbering on mass without triggering auto-renumbering
99
+ # but may end up with the tree being out of sync if you don't reorder all nodes
100
+ def set_reference(ref)
101
+ self.reference = ref
102
+ set(:reference => ref, :depth => depth)
103
+ end
104
+
105
+ # TODO - implement this
106
+ # increases the depth of the node if possible
107
+ # can't indent if there's nothing before it on the same level (to become the new parent)
108
+ def indent
109
+ end
110
+
111
+ # TODO - implement this
112
+ # decreases the depth of the node if possible
113
+ # can't outdent further than 1
114
+ def outdent
115
+ end
116
+
117
+ # returns the parent for the node
118
+ # so [1,2,3] would look for a node with reference of [1,2]
119
+ def parent
120
+ return if root?
121
+ query = query_for_reference(reference[0, depth-1])
122
+ query[:depth] = depth - 1
123
+ scoped_find.first(query)
124
+ end
125
+
126
+ def parent=(obj)
127
+ ref = obj.reference
128
+
129
+ if child = obj.children.last
130
+ ref << (child.reference.last + 1)
131
+ else
132
+ ref << 1
133
+ end
134
+
135
+ self.reference = ref
136
+ end
137
+
138
+ def root?
139
+ depth == 1
140
+ end
141
+
142
+ def root
143
+ scoped_find.first(:"reference.0" => reference[0], :depth => 1)
144
+ end
145
+
146
+ def roots
147
+ scoped_find.all(:depth => 1)
148
+ end
149
+
150
+ def ancestors
151
+ return if root?
152
+ scoped_find.all(:depth => {:"$lt" => depth}, :"reference.0" => reference[0])
153
+ end
154
+
155
+ def siblings
156
+ query = query_for_reference(reference[0, depth-1])
157
+ query[:depth] = depth
158
+ query[:id] = {:"$ne" => self.id}
159
+ scoped_find.all(query)
160
+ end
161
+
162
+ def previous_siblings
163
+ query = query_for_reference(reference[0, depth-1])
164
+ query[:"reference.#{depth-1}"] = {:"$lt" => reference.last + 1}
165
+ query[:depth] = depth
166
+ query[:id] = {:"$ne" => self.id}
167
+ scoped_find.all(query)
168
+ end
169
+
170
+ def next_siblings
171
+ query = query_for_reference(reference[0, depth-1])
172
+ query[:"reference.#{depth-1}"] = {:"$gt" => reference.last - 1}
173
+ query[:depth] = depth
174
+ query[:id] = {:"$ne" => self.id}
175
+ scoped_find.all(query)
176
+ end
177
+
178
+ def self_and_siblings
179
+ query = query_for_reference(reference[0, depth-1])
180
+ query[:depth] = depth
181
+ scoped_find.all(query)
182
+ end
183
+
184
+ def children
185
+ query = query_for_reference(reference[0, depth])
186
+ query[:depth] = depth + 1
187
+ scoped_find.all(query)
188
+ end
189
+
190
+ def descendants
191
+ query = query_for_reference(reference[0, depth])
192
+ query[:depth] = {:"$gt" => depth}
193
+ scoped_find.all(query)
194
+ end
195
+
196
+ def self_and_descendants
197
+ [self] + descendants
198
+ end
199
+
200
+ def is_ancestor_of?(other)
201
+ return false if other.depth <= depth
202
+ other.reference[0, depth] == reference
203
+ end
204
+
205
+ def is_or_is_ancestor_of?(other)
206
+ other == self || is_ancestor_of?(other)
207
+ end
208
+
209
+ def is_descendant_of?(other)
210
+ return false if other.depth >= depth
211
+ reference[0, other.depth] == other.reference
212
+ end
213
+
214
+ def is_or_is_descendant_of?(other)
215
+ other == self || is_descendant_of?(other)
216
+ end
217
+
218
+ def is_sibling_of?(other)
219
+ return false if other.depth != depth
220
+ reference[0, depth-1] == other.reference[0, depth-1]
221
+ end
222
+
223
+ def is_or_is_sibling_of?(other)
224
+ other == self || is_sibling_of?(other)
225
+ end
226
+
227
+ private
228
+
229
+ def query_for_reference(ref)
230
+ query = {}
231
+ ref.each_with_index do |r, i|
232
+ query[:"reference.#{i}"] = r
233
+ end
234
+ query
235
+ end
236
+
237
+ def scoped_find
238
+ if referenced_tree_options[:scope]
239
+ self.class.sort(:reference.asc).where(referenced_tree_options[:scope] => self[referenced_tree_options[:scope]])
240
+ else
241
+ self.class.sort(:reference.asc)
242
+ end
243
+ end
244
+
245
+ def assign_reference
246
+ return unless reference.blank?
247
+
248
+ if root_node = roots.last
249
+ self.reference = [root_node.reference[0] + 1]
250
+ else
251
+ self.reference = [1]
252
+ end
253
+ end
254
+
255
+ def delete_descendants_and_renumber_siblings
256
+ if @destroy_descendants
257
+ self.children.each do |child|
258
+ child.destroy_with_children
259
+ end
260
+ end
261
+
262
+ # TODO - should be able to do this without renumbering the whole tree
263
+ # it's more complicated just decrementing the subsequent nodes though, as some need to change levels
264
+ #
265
+ # query = query_for_reference(reference[0, depth-1])
266
+ # query[:"reference.#{depth-1}"] = {:"$gt" => reference.last - 1}
267
+ #
268
+ # self.class.decrement(
269
+ # query,
270
+ # {:"reference.#{depth-1}" => 1}
271
+ # )
272
+
273
+ renumber_tree
274
+ end
275
+
276
+ # TODO - massively excessive - only renumber the required points
277
+ # To do that we need to figure out where it's moving from/to etc...
278
+ def renumber_tree_if_reference_changed
279
+ renumber_tree if reference_changed?
280
+ end
281
+
282
+ def renumber_tree
283
+ scope = {}
284
+ if referenced_tree_options[:scope]
285
+ scope[referenced_tree_options[:scope]] = self[referenced_tree_options[:scope]]
286
+ end
287
+
288
+ self.class.renumber_tree(scope)
289
+ end
290
+
291
+ def reposition_subsequent_nodes
292
+ self.class.set(
293
+ query_for_reference(reference),
294
+ {:"reference.#{depth-1}" => (reference[depth-1] + 1)}
295
+ )
296
+ end
297
+ end
298
+ end
299
+ end
300
+ end
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mm-referenced-tree
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - Richard Livsey
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-11-19 00:00:00 +00:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: rspec
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :development
34
+ version_requirements: *id001
35
+ description:
36
+ email: richard@livsey.org
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files:
42
+ - README.rdoc
43
+ files:
44
+ - LICENSE
45
+ - Rakefile
46
+ - README.rdoc
47
+ - lib/mm-referenced-tree.rb
48
+ has_rdoc: true
49
+ homepage: http://github.com/rlivsey/mm-referenced-tree
50
+ licenses: []
51
+
52
+ post_install_message:
53
+ rdoc_options:
54
+ - --main
55
+ - README.rdoc
56
+ require_paths:
57
+ - lib
58
+ required_ruby_version: !ruby/object:Gem::Requirement
59
+ none: false
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ hash: 3
64
+ segments:
65
+ - 0
66
+ version: "0"
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ none: false
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ hash: 3
73
+ segments:
74
+ - 0
75
+ version: "0"
76
+ requirements: []
77
+
78
+ rubyforge_project:
79
+ rubygems_version: 1.3.7
80
+ signing_key:
81
+ specification_version: 3
82
+ summary: Yet another tree plugin for MongoMapper, built with an array of reference numbers
83
+ test_files: []
84
+