mongoid_acts_as_list 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ tags
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.vimrc.local ADDED
@@ -0,0 +1,2 @@
1
+ map <Leader>r :w\|!clear && bundle exec rspec %<cr>
2
+ map <Leader>R :w\|!clear && bundle exec rspec spec<cr>
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
data/README.markdown ADDED
@@ -0,0 +1,134 @@
1
+ ActsAsList for Mongoid
2
+ =====================
3
+
4
+ ## Description
5
+
6
+ Mongoid::ActsAsList provides the ability of ordering and sorting a number of objects in a list using Mongoid as an ODM.
7
+
8
+
9
+ ## Install
10
+
11
+ Place the following in your Gemfile:
12
+
13
+ ``` ruby
14
+ gem 'mongoid_acts_as_list', '~> 0.0.3'
15
+ ```
16
+
17
+ Then run `bundle install`
18
+
19
+
20
+ ## Configuration
21
+
22
+ Configure defaults values used by ActsAsList:
23
+
24
+
25
+ ``` ruby
26
+ Mongoid::ActsAsList.configure do |config|
27
+
28
+ # These are the default values. Modify as you see fit:
29
+
30
+ config.default_position_field = :position
31
+ config.start_list_at = 0
32
+
33
+ end
34
+ ```
35
+
36
+ Make sure it is loaded before calling ` acts_as_list `. You can place this code in an initializer file for example.
37
+
38
+ ## Usage
39
+
40
+ Activate ActsAsList in your models.
41
+
42
+ In has_many/belongs_to associations, you will need to provide a `:scope` option:
43
+
44
+ ``` ruby
45
+ class List
46
+ include Mongoid::Document
47
+
48
+ has_many :items
49
+ end
50
+
51
+ class Item
52
+ include Mongoid::Document
53
+ include Mongoid::ActsAsList
54
+
55
+ belongs_to :list
56
+ acts_as_list scope: :list
57
+ end
58
+ ```
59
+
60
+ On embedded document, the scope option is not necessary:
61
+
62
+ ``` ruby
63
+ class List
64
+ include Mongoid::Document
65
+
66
+ embeds_many :items
67
+ end
68
+
69
+ class Item
70
+ include Mongoid::Document
71
+ include Mongoid::ActsAsList
72
+
73
+ embedded_in :list
74
+ acts_as_list
75
+ end
76
+ ```
77
+
78
+
79
+ ``` ruby
80
+ ## Class Methods
81
+
82
+ list.items.order_by_position #=> returns all items in `list` ordered by position
83
+
84
+ ## Instance Methods
85
+
86
+ item.move to: 2 #=> moves item to the 2nd position
87
+ item.move to: :start #=> moves item to the first position in the list
88
+ item.move to: :end #=> moves item to the last position in the list
89
+ item.move before: other_item #=> moves item before other_item
90
+ item.move after: other_item #=> moves item after other_item
91
+ item.move forward: 3 #=> move item 3 positions closer to the end of the list
92
+ item.move backward: 2 #=> move item 2 positions closer to the start of the list
93
+ item.move :forward #=> same as item.move(forward: 1)
94
+ item.move :backward #=> same as item.move(backward: 1)
95
+
96
+ item.in_list? #=> true
97
+ item.remove_from_list #=> sets the position to nil and reorders other items
98
+ item.not_in_list? #=> true
99
+
100
+ item.first?
101
+ item.last?
102
+
103
+ item.next_item #=> returns the item immediately following `item` in the list
104
+ item.previous_item #=> returns the item immediately preceding `item` in the list
105
+ ```
106
+
107
+ Other methods are available, as well as all the methods from the original ActiveRecord ActsAsList gem.
108
+ Check the source and documentation to find out more!
109
+
110
+
111
+ ## Requirements
112
+
113
+ Tested with Mongoid 2.4.6 on Ruby 1.9.3-p125, Rails 3.2.2, and Mongo 2.x
114
+
115
+
116
+ ## Roadmap
117
+
118
+ * Test with several layers of embedding documents
119
+
120
+
121
+ ## Contributing
122
+
123
+ - Fork the project
124
+ - Start a feature/bugfix branch
125
+ - Start writing tests
126
+ - Commit and push until all tests are green and you are happy with your contribution
127
+
128
+ - If you're using Vim, source the .vimrc.local file. It provides 2 shortcuts for running the specs:
129
+ - `<Leader>r` runs the current spec file
130
+ - `<Leader>R` runs all spec files
131
+
132
+ ## Copyright
133
+
134
+ Copyright (c) 2012 Olivier Melcher, released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "mongoid/acts_as_list/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "mongoid_acts_as_list"
7
+ s.version = Mongoid::ActsAsList::VERSION
8
+ s.authors = ["Olivier Melcher"]
9
+ s.email = ["olivier.melcher@gmail.com"]
10
+ s.homepage = ""
11
+ s.summary = %q{Implementation of the acts as list gem for Mongoid}
12
+ s.description = %q{}
13
+
14
+ s.rubyforge_project = "acts_as_list"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ # specify any dependencies here; for example:
22
+ s.add_development_dependency "rspec"
23
+ s.add_development_dependency "bson_ext", "~> 1.5"
24
+ s.add_development_dependency "database_cleaner"
25
+ s.add_development_dependency "pry"
26
+ s.add_runtime_dependency "mongoid", [">= 2.0.1"]
27
+ end
@@ -0,0 +1,12 @@
1
+ module Mongoid
2
+ module ActsAsList
3
+ class Configuration
4
+ attr_accessor :default_position_field, :start_list_at
5
+
6
+ def initialize
7
+ @default_position_field = :position
8
+ @start_list_at = 0
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,47 @@
1
+ module Mongoid::ActsAsList
2
+ module List::Embedded
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ raise List::ScopeMissingError, "Mongoid::ActsAsList::Embedded can only be included in embedded documents" unless embedded?
7
+ end
8
+
9
+ module ClassMethods
10
+ private
11
+
12
+ def define_position_scope(scope_name)
13
+ define_method(:scope_condition) { {position_field.ne => nil} }
14
+ end
15
+ end
16
+
17
+ ## InstanceMethods
18
+ private
19
+
20
+ def shift_position options = {}
21
+ criteria = options.fetch(:for, to_criteria)
22
+ by_how_much = options.fetch(:by, 1)
23
+
24
+ criteria = criteria.to_criteria if criteria.is_a? self.class
25
+
26
+ criteria.each do |doc|
27
+ doc.inc(position_field, by_how_much)
28
+ end
29
+ end
30
+
31
+ def to_criteria
32
+ embedded_collection.where(_id: _id)
33
+ end
34
+
35
+ def items_in_list
36
+ embedded_collection.where(scope_condition)
37
+ end
38
+
39
+ def root_collection
40
+ _parent.db.collection(_parent.collection.name)
41
+ end
42
+
43
+ def embedded_collection
44
+ _parent.send(metadata.name)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,38 @@
1
+ module Mongoid::ActsAsList
2
+ module List
3
+ module Root
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+ private
8
+
9
+ def define_position_scope(scope_name)
10
+ raise List::ScopeMissingError, "#acts_as_list requires a scope option" if scope_name.blank?
11
+
12
+ scope_name = "#{scope_name}_id".intern if scope_name.to_s !~ /_id$/
13
+ define_method(:scope_condition) { {scope_name => self[scope_name]} }
14
+ end
15
+ end
16
+
17
+ ## InstanceMethods
18
+ private
19
+
20
+ def shift_position options = {}
21
+ criteria = options.fetch(:for, to_criteria)
22
+ by_how_much = options.fetch(:by, 1)
23
+
24
+ criteria = criteria.to_criteria if criteria.is_a? self.class
25
+
26
+ db.collection(collection.name).update(criteria.selector, {"$inc" => { position_field => by_how_much }}, {multi: true})
27
+ end
28
+
29
+ def to_criteria
30
+ self.class.where(_id: _id)
31
+ end
32
+
33
+ def items_in_list
34
+ self.class.where(scope_condition).and(position_field.ne => nil)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,295 @@
1
+ module Mongoid::ActsAsList
2
+ module List
3
+ extend ActiveSupport::Concern
4
+
5
+ autoload :Root , 'mongoid/acts_as_list/list/root.rb'
6
+ autoload :Embedded , 'mongoid/acts_as_list/list/embedded.rb'
7
+
8
+ class ScopeMissingError < RuntimeError; end
9
+
10
+ module ClassMethods
11
+ def acts_as_list options = {}
12
+ field = options.fetch(:field, Mongoid::ActsAsList.configuration.default_position_field).try(:to_sym)
13
+ scope = options.fetch(:scope, nil).try(:to_sym)
14
+
15
+ include list_submodule
16
+ define_position_field field
17
+ define_position_scope scope
18
+ end
19
+
20
+ def order_by_position(conditions = {}, order = :asc)
21
+ order, conditions = [conditions || :asc, {}] unless conditions.is_a? Hash
22
+ where( conditions ).order_by [[position_field, order], [:created_at, order]]
23
+ end
24
+
25
+ private
26
+
27
+ def list_submodule
28
+ embedded? ? Embedded : Root
29
+ end
30
+
31
+ def define_position_field(field_name)
32
+ field field_name, type: Integer
33
+
34
+ set_callback :validation, :before, if: -> { new? && not_in_list? } do |doc|
35
+ doc[field_name] = doc.send(:next_available_position_in_list)
36
+ end
37
+
38
+ set_callback :destroy, :after, :shift_later_items_towards_start_of_list, if: -> { in_list? }
39
+
40
+ [:define_method, :define_singleton_method].each do |define_method|
41
+ send(define_method, :position_field) { field_name }
42
+ end
43
+ end
44
+ end
45
+
46
+ ## InstanceMethods
47
+
48
+ # Public: Moves the item to new position in the list
49
+ #
50
+ # where - a Hash specifying where to move the item
51
+ # :to - an Integer representing a position number
52
+ # or a Symbol from the list :start, :top, :end, :bottom
53
+ # :before, :above - another object in the list
54
+ # :after: , :below - another object in the list
55
+ # :forward, :lower - an Integer specify by how much to move the item forward.
56
+ # will stop moving the item when it reaches the end of the list
57
+ # :backward, :higher - an Integer specify by how much to move the item forward.
58
+ # will stop moving the item when it reaches the end of the list
59
+ #
60
+ # or a Symbol in :forward, :lower, :backward, :higher
61
+ #
62
+ # Examples
63
+ #
64
+ # item.move to: 3
65
+ # #=> moves item to the 3rd position
66
+ #
67
+ # item.move to: :start
68
+ # #=> moves item to the first position in the list
69
+ #
70
+ # other_item.position #=> 3
71
+ #
72
+ # item.move before: other_item
73
+ # #=> moves item to position 3 and other_item to position 4
74
+ #
75
+ # item.move after: other_item
76
+ # #=> moves item to position 4
77
+ #
78
+ # item.move backward: 3
79
+ # #=> move item 3 positions closer to the start of the list
80
+ #
81
+ # item.move :forward
82
+ # #=> same as item.move(forward: 1)
83
+ #
84
+ # Returns nothing
85
+ def move(where = {})
86
+ if where.is_a? Hash
87
+ options = [:to, :before, :above, :after, :below, :forward, :forwards, :lower, :backward, :backwards, :higher]
88
+
89
+ prefix, destination = where.each.select { |k, _| options.include? k }.first
90
+ raise ArgumentError, "#move requires one of the following options: #{options.join(', ')}" unless prefix
91
+
92
+ send("move_#{prefix}", destination)
93
+ else
94
+ destination = where
95
+
96
+ send("move_#{destination}")
97
+ end
98
+ end
99
+
100
+ def move_to(destination)
101
+ if destination.is_a? Symbol
102
+ send("move_to_#{destination}")
103
+ else
104
+ destination = position_within_list_boundaries(destination)
105
+ insert_at destination
106
+ end
107
+ end
108
+
109
+ def move_to_end
110
+ new_position = in_list? ? last_position_in_list : next_available_position_in_list
111
+ insert_at new_position
112
+ end
113
+ alias_method :move_to_bottom, :move_to_end
114
+
115
+ def move_to_start
116
+ insert_at start_position_in_list
117
+ end
118
+ alias_method :move_to_top, :move_to_start
119
+
120
+ def move_forwards by_how_much = 1
121
+ move_to(self[position_field] + by_how_much) unless last?
122
+ end
123
+ alias_method :move_lower , :move_forwards
124
+ alias_method :move_forward, :move_forwards
125
+
126
+ def move_backwards by_how_much = 1
127
+ move_to(self[position_field] - by_how_much) unless first?
128
+ end
129
+ alias_method :move_higher , :move_backwards
130
+ alias_method :move_forward, :move_forwards
131
+
132
+ def move_before(other_item)
133
+ destination = other_item[position_field]
134
+ origin = self[position_field]
135
+
136
+ if origin > destination
137
+ insert_at destination
138
+ else
139
+ insert_at destination - 1
140
+ end
141
+ end
142
+ alias_method :move_above, :move_before
143
+
144
+ def move_after(other_item)
145
+ destination = other_item[position_field]
146
+ origin = self[position_field]
147
+
148
+ if origin > destination
149
+ insert_at destination + 1
150
+ else
151
+ insert_at destination
152
+ end
153
+ end
154
+ alias_method :move_below, :move_after
155
+
156
+ # Public: Removes the item from the list
157
+ #
158
+ # Returns true if the item was removed, false if not
159
+ def remove_from_list
160
+ return true unless in_list?
161
+ shift_later_items_towards_start_of_list
162
+ update_attributes(position_field => nil)
163
+ end
164
+
165
+ # Public: Indicates if an item is in the list
166
+ #
167
+ # Returns true if the item is in the list or false if not
168
+ def in_list?
169
+ self[position_field].present?
170
+ end
171
+
172
+ # Public: Indicates if an item is not in the list
173
+ #
174
+ # Returns true if the item is not in the list or false if it is
175
+ def not_in_list?
176
+ !in_list?
177
+ end
178
+
179
+ # Public: Indicates if an item is the first of the list
180
+ #
181
+ # Returns true if the item is the first in the list or false if not
182
+ def first?
183
+ self[position_field] == start_position_in_list
184
+ end
185
+
186
+ # Public: Indicates if an item is the last of the list
187
+ #
188
+ # Returns true if the item is the last in the list or false if not
189
+ def last?
190
+ self[position_field] == last_item_in_list[position_field]
191
+ end
192
+
193
+ # Public: Gets the following item in the list
194
+ #
195
+ # Returns the next item in the list
196
+ # or nil if there isn't a next item
197
+ def next_item
198
+ return unless in_list?
199
+ items_in_list.where(position_field => self[position_field]+1).first
200
+ end
201
+ alias_method :higher_item, :next_item
202
+
203
+ # Public: Gets the preceding item in the list
204
+ #
205
+ # Returns the previous item in the list
206
+ # or nil if there isn't a previous item
207
+ def previous_item
208
+ return unless in_list?
209
+ items_in_list.where(position_field => self[position_field]-1).first
210
+ end
211
+ alias_method :lower_item, :previous_item
212
+
213
+ # Public: Insert at a given position in the list
214
+ #
215
+ # new_position - an Integer indicating the position to insert the item at
216
+ #
217
+ # Returns nothing
218
+ def insert_at(new_position)
219
+ insert_space_at(new_position)
220
+ update_attribute(position_field, new_position)
221
+ end
222
+
223
+ private
224
+
225
+ # Internal: Make space in the list at a given position number
226
+ # used when moving a item to a new position in the list.
227
+ #
228
+ # position - an Integer representing the position number
229
+ #
230
+ # Returns nothing
231
+ def insert_space_at(position)
232
+ from = self[position_field] || next_available_position_in_list
233
+ to = position
234
+
235
+ if from < to
236
+ shift_position for: items_between(from, to + 1), by: -1
237
+ else
238
+ shift_position for: items_between(to - 1, from), by: 1
239
+ end
240
+ end
241
+
242
+ def items_between(from, to, options = {})
243
+ strict = options.fetch(:strict, true)
244
+ if strict
245
+ items_in_list.where(position_field.gt => from, position_field.lt => to)
246
+ else
247
+ items_in_list.where(position_field.gte => from, position_field.lte => to)
248
+ end
249
+ end
250
+
251
+ def last_item_in_list
252
+ items_in_list.order_by_position.last
253
+ end
254
+
255
+ def last_position_in_list
256
+ last_item_in_list.try(position_field)
257
+ end
258
+
259
+ def previous_items_in_list
260
+ items_in_list.where(position_field.lt => self[position_field])
261
+ end
262
+
263
+ def next_items_in_list
264
+ items_in_list.where(position_field.gt => self[position_field])
265
+ end
266
+
267
+ def shift_later_items_towards_start_of_list
268
+ return unless in_list?
269
+ shift_position for: next_items_in_list, by: -1
270
+ end
271
+
272
+ def next_available_position_in_list
273
+ if item = last_item_in_list
274
+ item[position_field] + 1
275
+ else
276
+ start_position_in_list
277
+ end
278
+ end
279
+
280
+ def first_position_in_list
281
+ Mongoid::ActsAsList.configuration.start_list_at
282
+ end
283
+ alias_method :start_position_in_list, :first_position_in_list
284
+
285
+ def position_within_list_boundaries(position)
286
+ if position < start_position_in_list
287
+ position = start_position_in_list
288
+ elsif position > last_position_in_list
289
+ position = last_position_in_list
290
+ end
291
+
292
+ position
293
+ end
294
+ end
295
+ end
@@ -0,0 +1,5 @@
1
+ module Mongoid
2
+ module ActsAsList
3
+ VERSION = "0.0.3"
4
+ end
5
+ end
@@ -0,0 +1,21 @@
1
+ require_relative 'mongoid/acts_as_list/list'
2
+ require_relative 'mongoid/acts_as_list/configuration'
3
+ require_relative 'mongoid/acts_as_list/version'
4
+
5
+ module Mongoid
6
+ module ActsAsList
7
+ class << self
8
+ attr_accessor :configuration
9
+
10
+ def configure
11
+ self.configuration ||= Configuration.new
12
+ yield(configuration) if block_given?
13
+ end
14
+
15
+ def included base
16
+ self.configure
17
+ base.send :include, List
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,47 @@
1
+ require 'spec_helper'
2
+
3
+ describe Mongoid::ActsAsList::List do
4
+ [:position, :number].each do |default_field_name|
5
+ let(:position_field) { default_field_name }
6
+
7
+ before do
8
+ Mongoid::ActsAsList.configure do |config|
9
+ config.default_position_field = position_field
10
+ end
11
+
12
+ require 'fixtures/embeds_many_models'
13
+ end
14
+
15
+ describe Mongoid::ActsAsList::List::Embedded do
16
+ let(:category_1) { Category.create! }
17
+ let(:category_2) { Category.create! }
18
+ let(:category_3) { Category.create! }
19
+
20
+ before do
21
+ [category_1, category_2].each do |cat|
22
+ 3.times do |n|
23
+ cat.items.create! position_field => n
24
+ end
25
+ cat.should have(3).items
26
+ end
27
+ end
28
+
29
+ it "should be embedded" do
30
+ EmbeddedItem.should be_embedded
31
+ end
32
+
33
+ it "should not include ActsAsList::Relational" do
34
+ EmbeddedItem.included_modules.should_not include Mongoid::ActsAsList::List::Root
35
+ end
36
+
37
+ it_behaves_like 'a list'
38
+
39
+ describe ".acts_as_list" do
40
+ it "defines #scope_condition" do
41
+ item = category_1.items.first
42
+ item.scope_condition.should == {position_field.ne => nil}
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end