acts_as_silent_list 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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