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.
- data/LICENSE +20 -0
- data/README.rdoc +36 -0
- data/Rakefile +74 -0
- data/lib/mm-referenced-tree.rb +300 -0
- 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
|
+
|