mongoid_acts_as_list 0.0.3

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/.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