acts_as_silent_list 1.0.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/.gemtest ADDED
File without changes
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .rvmrc
6
+ *.tmproj
data/CHANGELOG.rdoc ADDED
@@ -0,0 +1,4 @@
1
+ = 1.0.0 - Initial Release
2
+
3
+ This gem is based on acts_as_list, while it tries to avoid triggering
4
+ ActiveRecord callbacks.
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :rubygems
2
+
3
+ gemspec
data/README.rdoc ADDED
@@ -0,0 +1,35 @@
1
+ = ActsAsSilentList
2
+
3
+ == Description
4
+
5
+ This acts_as extension provides the capabilities for sorting and reordering a
6
+ number of objects in a list. The class that has this specified needs to have a
7
+ +position+ column defined as an integer on the mapped database table.
8
+
9
+ This project is a fork of the well-known acts_as_list. This version avoids
10
+ triggering ActiveRecord's callback mechanisms, such that your list may be
11
+ managed silently.
12
+
13
+ == Example
14
+
15
+ class TodoList < ActiveRecord::Base
16
+ has_many :todo_items, :order => "position"
17
+ end
18
+
19
+ class TodoItem < ActiveRecord::Base
20
+ belongs_to :todo_list
21
+ acts_as_silent_list :scope => :todo_list
22
+ end
23
+
24
+ todo_list.first.move_to_bottom
25
+ todo_list.last.move_higher
26
+
27
+ == Contributing to acts_as_silent_list
28
+
29
+ Open an issue in the project's issue tracker on GitHub or open a pull request
30
+ using.
31
+
32
+ == Copyright
33
+
34
+ Copyright (c) 2007 David Heinemeier Hansson, released under the MIT license
35
+
data/Rakefile ADDED
@@ -0,0 +1,31 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ #require 'rake'
5
+ require 'rake/testtask'
6
+
7
+ # Run the test with 'rake' or 'rake test'
8
+ desc 'Default: run acts_as_silent_list unit tests.'
9
+ task :default => :test
10
+
11
+ desc 'Test the acts_as_silent_list plugin.'
12
+ Rake::TestTask.new(:test) do |t|
13
+ t.libs << 'lib' << 'test'
14
+ t.pattern = 'test/**/test_*.rb'
15
+ t.verbose = true
16
+ end
17
+
18
+
19
+
20
+ # Run the rdoc task to generate rdocs for this gem
21
+ require 'rdoc/task'
22
+ RDoc::Task.new do |rdoc|
23
+ require "acts_as_silent_list/version"
24
+ version = ActiveRecord::Acts::SilentList::VERSION
25
+
26
+ rdoc.rdoc_dir = 'rdoc'
27
+ rdoc.title = "acts_as_silent_list #{version}"
28
+ rdoc.rdoc_files.include('README*')
29
+ rdoc.rdoc_files.include('lib/**/*.rb')
30
+ end
31
+
@@ -0,0 +1,31 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path('../lib', __FILE__)
3
+ require 'acts_as_silent_list/version'
4
+
5
+ Gem::Specification.new do |s|
6
+
7
+ # Description Meta...
8
+ s.name = 'acts_as_silent_list'
9
+ s.version = ActiveRecord::Acts::SilentList::VERSION
10
+ s.platform = Gem::Platform::RUBY
11
+ s.authors = ['David Heinemeier Hansson', 'Swanand Pagnis', 'Quinn Chaffee', 'Gregor Schmidt']
12
+ s.email = ['swanand.pagnis@gmail.com']
13
+ s.homepage = 'http://github.com/swanandp/acts_as_silent_list'
14
+ s.summary = %q{A gem allowing a active_record model to be ordered, without triggering active record callbacks.}
15
+ s.description = %q{This "acts_as" extension is a clone of the well known acts_as_list, only it avoids triggering active record callbacks.}
16
+
17
+
18
+ # Load Paths...
19
+ s.files = `git ls-files`.split("\n")
20
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
21
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
22
+ s.require_paths = ['lib']
23
+
24
+
25
+ # Dependencies (installed via 'bundle install')...
26
+ s.add_development_dependency("bundler", ["~> 1.0.0"])
27
+ s.add_development_dependency("activerecord", [">= 1.15.4.7794"])
28
+ s.add_development_dependency("rake")
29
+ s.add_development_dependency("rdoc")
30
+ s.add_development_dependency("sqlite3")
31
+ end
data/init.rb ADDED
@@ -0,0 +1,2 @@
1
+ $:.unshift "#{File.dirname(__FILE__)}/lib"
2
+ require 'acts_as_silent_list'
@@ -0,0 +1,2 @@
1
+ require 'acts_as_silent_list/active_record/acts/silent_list'
2
+ ActiveRecord::Base.class_eval { include ActiveRecord::Acts::SilentList }
@@ -0,0 +1,308 @@
1
+ module ActiveRecord
2
+ module Acts #:nodoc:
3
+ module SilentList #:nodoc:
4
+ def self.included(base)
5
+ base.extend(ClassMethods)
6
+ end
7
+
8
+ # This +acts_as+ extension provides the capabilities for sorting and reordering a number of objects in a list.
9
+ # The class that has this specified needs to have a +position+ column defined as an integer on
10
+ # the mapped database table.
11
+ #
12
+ # Todo list example:
13
+ #
14
+ # class TodoList < ActiveRecord::Base
15
+ # has_many :todo_items, :order => "position"
16
+ # end
17
+ #
18
+ # class TodoItem < ActiveRecord::Base
19
+ # belongs_to :todo_list
20
+ # acts_as_silent_list :scope => :todo_list
21
+ # end
22
+ #
23
+ # todo_list.first.move_to_bottom
24
+ # todo_list.last.move_higher
25
+ module ClassMethods
26
+ # Configuration options are:
27
+ #
28
+ # * +column+ - specifies the column name to use for keeping the position integer (default: +position+)
29
+ # * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach <tt>_id</tt>
30
+ # (if it hasn't already been added) and use that as the foreign key restriction. It's also possible
31
+ # to give it an entire string that is interpolated if you need a tighter scope than just a foreign key.
32
+ # Example: <tt>acts_as_silent_list :scope => 'todo_list_id = #{todo_list_id} AND completed = 0'</tt>
33
+ # * +top_of_list+ - defines the integer used for the top of the list. Defaults to 1. Use 0 to make the collection
34
+ # act more like an array in its indexing.
35
+ def acts_as_silent_list(options = {})
36
+ configuration = { :column => "position", :scope => "1 = 1", :top_of_list => 1}
37
+ configuration.update(options) if options.is_a?(Hash)
38
+
39
+ configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/
40
+
41
+ if configuration[:scope].is_a?(Symbol)
42
+ scope_condition_method = %(
43
+ def scope_condition
44
+ self.class.send(:sanitize_sql_hash_for_conditions, { :#{configuration[:scope].to_s} => send(:#{configuration[:scope].to_s}) })
45
+ end
46
+ )
47
+ elsif configuration[:scope].is_a?(Array)
48
+ scope_condition_method = %(
49
+ def scope_condition
50
+ attrs = %w(#{configuration[:scope].join(" ")}).inject({}) do |memo,column|
51
+ memo[column.intern] = send(column.intern); memo
52
+ end
53
+ self.class.send(:sanitize_sql_hash_for_conditions, attrs)
54
+ end
55
+ )
56
+ else
57
+ scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end"
58
+ end
59
+
60
+ class_eval <<-EOV
61
+ include ActiveRecord::Acts::SilentList::InstanceMethods
62
+
63
+ def acts_as_silent_list_top
64
+ #{configuration[:top_of_list]}.to_i
65
+ end
66
+
67
+ def acts_as_silent_list_class
68
+ ::#{self.name}
69
+ end
70
+
71
+ def position_column
72
+ '#{configuration[:column]}'
73
+ end
74
+
75
+ #{scope_condition_method}
76
+
77
+ before_destroy :decrement_positions_on_lower_items
78
+ before_create :add_to_list_bottom
79
+
80
+ def self.unscoped
81
+ raise NotImplementedError, "This is just a stub implementation, update to Rails 3 to get the whole thing." unless block_given?
82
+ with_exclusive_scope { yield }
83
+ end unless defined? unscoped
84
+ EOV
85
+ end
86
+ end
87
+
88
+ # All the methods available to a record that has had <tt>acts_as_silent_list</tt> specified. Each method works
89
+ # by assuming the object to be the item in the list, so <tt>chapter.move_lower</tt> would move that chapter
90
+ # lower in the list of all chapters. Likewise, <tt>chapter.first?</tt> would return +true+ if that chapter is
91
+ # the first in the list of all chapters.
92
+ module InstanceMethods
93
+ # Insert the item at the given position (defaults to the top position of 1).
94
+ def insert_at(position = acts_as_silent_list_top)
95
+ insert_at_position(position)
96
+ end
97
+
98
+ # Swap positions with the next lower item, if one exists.
99
+ def move_lower
100
+ return unless lower_item
101
+
102
+ acts_as_silent_list_class.transaction do
103
+ lower_item.decrement_position
104
+ increment_position
105
+ end
106
+ end
107
+
108
+ # Swap positions with the next higher item, if one exists.
109
+ def move_higher
110
+ return unless higher_item
111
+
112
+ acts_as_silent_list_class.transaction do
113
+ higher_item.increment_position
114
+ decrement_position
115
+ end
116
+ end
117
+
118
+ # Move to the bottom of the list. If the item is already in the list, the items below it have their
119
+ # position adjusted accordingly.
120
+ def move_to_bottom
121
+ return unless in_list?
122
+ acts_as_silent_list_class.transaction do
123
+ decrement_positions_on_lower_items
124
+ assume_bottom_position
125
+ end
126
+ end
127
+
128
+ # Move to the top of the list. If the item is already in the list, the items above it have their
129
+ # position adjusted accordingly.
130
+ def move_to_top
131
+ return unless in_list?
132
+ acts_as_silent_list_class.transaction do
133
+ increment_positions_on_higher_items
134
+ assume_top_position
135
+ end
136
+ end
137
+
138
+ # Removes the item from the list.
139
+ def remove_from_list
140
+ if in_list?
141
+ decrement_positions_on_lower_items
142
+ update_attribute_silently(position_column, nil)
143
+ end
144
+ end
145
+
146
+ # Increase the position of this item without adjusting the rest of the list.
147
+ def increment_position
148
+ return unless in_list?
149
+ update_attribute_silently(position_column, self.send(position_column).to_i + 1)
150
+ end
151
+
152
+ # Decrease the position of this item without adjusting the rest of the list.
153
+ def decrement_position
154
+ return unless in_list?
155
+ update_attribute_silently(position_column, self.send(position_column).to_i - 1)
156
+ end
157
+
158
+ # Return +true+ if this object is the first in the list.
159
+ def first?
160
+ return false unless in_list?
161
+ self.send(position_column) == acts_as_silent_list_top
162
+ end
163
+
164
+ # Return +true+ if this object is the last in the list.
165
+ def last?
166
+ return false unless in_list?
167
+ self.send(position_column) == bottom_position_in_list
168
+ end
169
+
170
+ # Return the next higher item in the list.
171
+ def higher_item
172
+ return nil unless in_list?
173
+ acts_as_silent_list_class.find(:first, :conditions =>
174
+ "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i - 1).to_s}"
175
+ )
176
+ end
177
+
178
+ # Return the next lower item in the list.
179
+ def lower_item
180
+ return nil unless in_list?
181
+ acts_as_silent_list_class.find(:first, :conditions =>
182
+ "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i + 1).to_s}"
183
+ )
184
+ end
185
+
186
+ # Test if this record is in a list
187
+ def in_list?
188
+ !not_in_list?
189
+ end
190
+
191
+ def not_in_list?
192
+ send(position_column).nil?
193
+ end
194
+
195
+ def default_position
196
+ acts_as_silent_list_class.columns_hash[position_column.to_s].default
197
+ end
198
+
199
+ def default_position?
200
+ default_position == send(position_column)
201
+ end
202
+
203
+ private
204
+ def add_to_list_top
205
+ increment_positions_on_all_items
206
+ end
207
+
208
+ def add_to_list_bottom
209
+ if not_in_list? || default_position?
210
+ self[position_column] = bottom_position_in_list.to_i + 1
211
+ else
212
+ increment_positions_on_lower_items(self[position_column])
213
+ end
214
+ end
215
+
216
+ # Overwrite this method to define the scope of the list changes
217
+ def scope_condition() "1" end
218
+
219
+ # Returns the bottom position number in the list.
220
+ # bottom_position_in_list # => 2
221
+ def bottom_position_in_list(except = nil)
222
+ item = bottom_item(except)
223
+ item ? item.send(position_column) : acts_as_silent_list_top - 1
224
+ end
225
+
226
+ # Returns the bottom item
227
+ def bottom_item(except = nil)
228
+ conditions = scope_condition
229
+ conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except
230
+ acts_as_silent_list_class.unscoped do
231
+ acts_as_silent_list_class.find(:first, :conditions => conditions, :order => "#{position_column} DESC")
232
+ end
233
+ end
234
+
235
+ # Forces item to assume the bottom position in the list.
236
+ def assume_bottom_position
237
+ update_attribute_silently(position_column, bottom_position_in_list(self).to_i + 1)
238
+ end
239
+
240
+ # Forces item to assume the top position in the list.
241
+ def assume_top_position
242
+ update_attribute_silently(position_column, acts_as_silent_list_top)
243
+ end
244
+
245
+ # This has the effect of moving all the higher items up one.
246
+ def decrement_positions_on_higher_items(position)
247
+ acts_as_silent_list_class.update_all(
248
+ "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} <= #{position}"
249
+ )
250
+ end
251
+
252
+ # This has the effect of moving all the lower items up one.
253
+ def decrement_positions_on_lower_items
254
+ return unless in_list?
255
+ acts_as_silent_list_class.update_all(
256
+ "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} > #{send(position_column).to_i}"
257
+ )
258
+ end
259
+
260
+ # This has the effect of moving all the higher items down one.
261
+ def increment_positions_on_higher_items
262
+ return unless in_list?
263
+ acts_as_silent_list_class.update_all(
264
+ "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} < #{send(position_column).to_i}"
265
+ )
266
+ end
267
+
268
+ # This has the effect of moving all the lower items down one.
269
+ def increment_positions_on_lower_items(position)
270
+ acts_as_silent_list_class.update_all(
271
+ "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} >= #{position}"
272
+ )
273
+ end
274
+
275
+ # Increments position (<tt>position_column</tt>) of all items in the list.
276
+ def increment_positions_on_all_items
277
+ acts_as_silent_list_class.update_all(
278
+ "#{position_column} = (#{position_column} + 1)", "#{scope_condition}"
279
+ )
280
+ end
281
+
282
+ def insert_at_position(position)
283
+ store_at_0
284
+ increment_positions_on_lower_items(position)
285
+ update_attribute_silently(position_column, position)
286
+ end
287
+
288
+ # used by insert_at_position instead of remove_from_list, as postgresql raises error if position_column has non-null constraint
289
+ def store_at_0
290
+ if in_list?
291
+ decrement_positions_on_lower_items
292
+ update_attribute_silently(position_column, 0)
293
+ end
294
+ end
295
+
296
+ def update_attribute_silently(attr, value)
297
+ if respond_to? :write_attribute_without_dirty
298
+ write_attribute_without_dirty(attr, value)
299
+ else
300
+ write_attribute(attr, value)
301
+ changed_attributes.delete(attr)
302
+ end
303
+ self.class.update_all({attr => value}, {:id => id})
304
+ end
305
+ end
306
+ end
307
+ end
308
+ end
@@ -0,0 +1,7 @@
1
+ module ActiveRecord
2
+ module Acts
3
+ module SilentList
4
+ VERSION = "1.0.0"
5
+ end
6
+ end
7
+ end