webpulser-habtm_list 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README +42 -0
  3. data/Rakefile +22 -0
  4. data/lib/webpulser-habtm_list.rb +315 -0
  5. metadata +58 -0
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 [name of plugin creator]
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 ADDED
@@ -0,0 +1,42 @@
1
+ HabtmList
2
+ =========
3
+
4
+ This plugin arose out of a page on the Rails wiki:
5
+ http://wiki.rubyonrails.org/rails/pages/BetterHabtmList
6
+
7
+ The purpose is to allow position-based "acts_as_list"-like behavior in a has_and_belongs_to_many association. Methods are included to move members up and down in the list.
8
+
9
+ The original author of the technique, according to the wiki history, was "Matt." Sorry, I don't know his last name; I'll give proper credit if he turns up to take it. I refactored and fixed some bugs in the code, and now I'm simply putting it up on Github. I'm claiming no ownership rights on this code whatsoever; use it any way you like.
10
+
11
+
12
+ Usage
13
+ -----
14
+ class Category < ActiveRecord::Base
15
+ has_and_belongs_to_many :products,
16
+ :order => 'position',
17
+ :list => true
18
+ end
19
+
20
+ Methods
21
+ -------
22
+ *first?(item)* - returns true if item is first in the list
23
+ *last?(item)* - returns true if item is last in the list
24
+ *in_list?(item)* - returns true if item exists in the list
25
+ *higher_item(item)* - returns the item one position above the given item in the list
26
+ *lower_item(item)* - returns the item one position below the given item in the list
27
+ *move_higher(item)* - moves the item up one position in the list
28
+ *move_lower(item)* - moves the item down one position in the list
29
+ *move_to_top(item)* - moves the item to the first position in the list
30
+ *move_to_bottom(item)* - moves the item to the last position in the list
31
+ *add_to_list_top(item)* - adds the item to the list in the first position
32
+ *add_to_list_bottom(item)* - adds the item to the list in the last position
33
+ *move_to_position(item, position)* - moves the item to the stated position in the list
34
+ *reset_positions* - "first aid" method to restore list positions if positions become duplicated or go out of sync (e.g., if records are inserted or updated by external methods)
35
+
36
+ Support
37
+ -------
38
+ I don't promise any, but if you have any questions or complaints you're welcome to e-mail me at sfeley@gmail.com.
39
+
40
+ Also check out my podcast, Escape Pod [http://escapepod.org], if you're interested in science fiction short stories.
41
+
42
+ - Steve Eley
data/Rakefile ADDED
@@ -0,0 +1,22 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ desc 'Default: run unit tests.'
6
+ task :default => :test
7
+
8
+ desc 'Test the habtm_list plugin.'
9
+ Rake::TestTask.new(:test) do |t|
10
+ t.libs << 'lib'
11
+ t.pattern = 'test/**/*_test.rb'
12
+ t.verbose = true
13
+ end
14
+
15
+ desc 'Generate documentation for the habtm_list plugin.'
16
+ Rake::RDocTask.new(:rdoc) do |rdoc|
17
+ rdoc.rdoc_dir = 'rdoc'
18
+ rdoc.title = 'HabtmList'
19
+ rdoc.options << '--line-numbers' << '--inline-source'
20
+ rdoc.rdoc_files.include('README')
21
+ rdoc.rdoc_files.include('lib/**/*.rb')
22
+ end
@@ -0,0 +1,315 @@
1
+ module RailsExtensions
2
+ module HabtmList
3
+
4
+ def self.append_features(base) #:nodoc:
5
+ super
6
+ base.extend(ClassMethods)
7
+ base.class_eval do
8
+ class << self
9
+ alias_method_chain :has_and_belongs_to_many, :list_handling
10
+ end
11
+ end
12
+ end
13
+
14
+ module ClassMethods
15
+ def has_and_belongs_to_many_with_list_handling(name, options={}, &extension)
16
+ if options.delete(:list)
17
+ options[:extend] = RailsExtensions::HabtmList::AssociationListMethods
18
+
19
+ after_add_callback_symbol = "maintain_list_after_add_for_#{name}".to_sym
20
+ before_remove_callback_symbol = "maintain_list_before_remove_for_#{name}".to_sym
21
+ name_ids = "#{name.to_s.singularize}_ids"
22
+ name_ids_symbol = ":#{name_ids}"
23
+
24
+ options[:after_add] ||= []
25
+ options[:after_add] << after_add_callback_symbol
26
+
27
+ options[:before_remove] ||= []
28
+ options[:before_remove] << before_remove_callback_symbol
29
+
30
+ class_eval <<-EOV
31
+ def #{after_add_callback_symbol}(added)
32
+ self.#{name}.add_to_list_bottom(added)
33
+ end
34
+
35
+ def #{before_remove_callback_symbol}(removed)
36
+ self.#{name}.remove_from_list(removed)
37
+ end
38
+
39
+ def update_attributes_with_#{name_ids}(params)
40
+ if params[#{name_ids_symbol}].kind_of?(Array)
41
+ @list_ids ||= {}
42
+ @list_ids[#{name_ids_symbol}] = params[#{name_ids_symbol}].reject(&:blank?)
43
+ end
44
+
45
+ update_attributes_without_#{name_ids}(params)
46
+ end
47
+
48
+ alias_method_chain :update_attributes, #{name_ids_symbol}
49
+ after_save :reset_#{name}_position
50
+
51
+ def reset_#{name}_position
52
+ if @list_ids && @list_ids[#{name_ids_symbol}]
53
+ self.#{name}.reset_positions_by_ids @list_ids[#{name_ids_symbol}]
54
+ end
55
+ end
56
+ EOV
57
+ end
58
+
59
+ has_and_belongs_to_many_without_list_handling(name, options, &extension)
60
+ end
61
+ end
62
+
63
+ module AssociationListMethods
64
+ def move_to_position(item, position)
65
+ return if !in_list?(item) || position.to_i == list_position(item)
66
+ list_item_class.transaction do
67
+ remove_from_list(item)
68
+ insert_at_position(item, position)
69
+ end
70
+ resort_array
71
+ end
72
+
73
+ def move_lower(item)
74
+ list_item_class.transaction do
75
+ lower = lower_item(item)
76
+ return unless lower
77
+ decrement_position(lower)
78
+ increment_position(item)
79
+ end
80
+ resort_array
81
+ end
82
+
83
+ def move_higher(item)
84
+ list_item_class.transaction do
85
+ higher = higher_item(item)
86
+ return unless higher
87
+ increment_position(higher)
88
+ decrement_position(item)
89
+ end
90
+ resort_array
91
+ end
92
+
93
+ def move_to_bottom(item)
94
+ return unless in_list?(item)
95
+ list_item_class.transaction do
96
+ decrement_positions_on_lower_items(item)
97
+ assume_bottom_position(item)
98
+ end
99
+ resort_array
100
+ end
101
+
102
+ def move_to_top(item)
103
+ return unless in_list?(item)
104
+ list_item_class.transaction do
105
+ increment_positions_on_higher_items(item)
106
+ assume_top_position(item)
107
+ end
108
+ resort_array
109
+ end
110
+
111
+ # should only be called externally from the before_remove callback
112
+ def remove_from_list(item)
113
+ decrement_positions_on_lower_items(item) if in_list?(item)
114
+ item[position_column] = nil
115
+ end
116
+
117
+ def first?(item)
118
+ item == self.first
119
+ end
120
+
121
+ def last?(item)
122
+ item == self.last
123
+ end
124
+
125
+ def higher_item(item)
126
+ return nil unless in_list?(item)
127
+ self.find(:first, :conditions => "#{position_column} = #{(list_position(item) - 1).to_s}")
128
+ end
129
+
130
+ def lower_item(item)
131
+ return nil unless in_list?(item)
132
+ self.find(:first, :conditions => "#{position_column} = #{(list_position(item) + 1).to_s}")
133
+ end
134
+
135
+ def in_list?(item)
136
+ self.include?(item)
137
+ end
138
+
139
+ def add_to_list_bottom(item)
140
+ item.save! if item.id.nil? # Rails 2.0.2 - Callbacks don't save first on association.create()
141
+ list_item_class.transaction do
142
+ assume_bottom_position(item)
143
+ end
144
+ resort_array
145
+ end
146
+
147
+ def add_to_list_top(item)
148
+ list_item_class.transaction do
149
+ increment_positions_on_all_items
150
+ assume_top_position(item)
151
+ end
152
+ resort_array
153
+ end
154
+
155
+ # "First aid" method in case someone shifts the array around outside these methods, or
156
+ # the positions in the joins table go totally out of whack. Don't use it for
157
+ # simple ordering because it's inefficient.
158
+ def reset_positions
159
+ self.each_index do |i|
160
+ item = self[i]
161
+ connection.update(
162
+ "UPDATE #{join_table} SET #{position_column} = #{i} " +
163
+ "WHERE #{foreign_key} = #{@owner.id} AND #{list_item_foreign_key} = #{item.id}"
164
+ )
165
+ end
166
+ end
167
+
168
+ def reset_positions_by_ids(ids = self.collect(&:id))
169
+ ids.each_with_index do |id, i|
170
+ connection.update(
171
+ "UPDATE #{join_table} SET #{position_column} = #{i} " +
172
+ "WHERE #{foreign_key} = #{@owner.id} AND #{list_item_foreign_key} = #{id}"
173
+ ) if id.to_i != 0
174
+ end
175
+ end
176
+
177
+
178
+ private
179
+ def position_column
180
+ @reflection.options[:order] || 'position'
181
+ end
182
+
183
+ def list_item_class
184
+ @reflection.klass
185
+ end
186
+
187
+ def join_table
188
+ @reflection.options[:join_table]
189
+ end
190
+
191
+ def foreign_key
192
+ @reflection.primary_key_name
193
+ end
194
+
195
+ def list_item_foreign_key
196
+ @reflection.association_foreign_key
197
+ end
198
+
199
+ def list_position(item)
200
+ self.index(item)
201
+ end
202
+
203
+
204
+ def set_position(item, position)
205
+ connection.update(
206
+ "UPDATE #{join_table} SET #{position_column} = #{position} " +
207
+ "WHERE #{foreign_key} = #{@owner.id} AND #{list_item_foreign_key} = #{item.id}"
208
+ )
209
+ if @target
210
+ obj = @target.find {|obj| obj.id == item.id}
211
+ obj[position_column] = position if obj
212
+ end
213
+ end
214
+
215
+ def assume_bottom_position(item)
216
+ set_position(item, self.length - 1)
217
+ end
218
+
219
+ def assume_top_position(item)
220
+ set_position(item, 0)
221
+ end
222
+
223
+ def increment_position_by(item, increment)
224
+ return unless in_list?(item)
225
+ connection.update(
226
+ "UPDATE #{join_table} SET #{position_column} = #{position_column} + (#{increment}) " +
227
+ "WHERE #{foreign_key} = #{@owner.id} AND #{list_item_foreign_key} = #{item.id}"
228
+ )
229
+ if @target
230
+ obj = @target.find {|obj| obj.id == item.id}
231
+ obj[position_column] = obj[position_column].to_i + increment if obj
232
+ end
233
+ end
234
+
235
+ def increment_position(item)
236
+ increment_position_by(item, 1)
237
+ end
238
+
239
+ def decrement_position(item)
240
+ increment_position_by(item, -1)
241
+ end
242
+
243
+ # This has the effect of moving all the higher items up one.
244
+ def decrement_positions_on_higher_items(position)
245
+ connection.update(
246
+ "UPDATE #{join_table} SET #{position_column} = (#{position_column} - 1) " +
247
+ "WHERE #{foreign_key} = #{@owner.id} AND #{position_column} <= #{position}"
248
+ )
249
+ @target.each { |obj|
250
+ obj[position_column] = obj[position_column].to_i - 1 if in_list?(obj) && obj[position_column].to_i <= position
251
+ } if @target
252
+ end
253
+
254
+ # This has the effect of moving all the lower items up one.
255
+ def decrement_positions_on_lower_items(item)
256
+ return unless in_list?(item)
257
+ position = list_position(item)
258
+ connection.update(
259
+ "UPDATE #{join_table} SET #{position_column} = (#{position_column} - 1) " +
260
+ "WHERE #{foreign_key} = #{@owner.id} AND #{position_column} > #{position}"
261
+ )
262
+ @target.each { |obj|
263
+ obj[position_column] = obj[position_column].to_i - 1 if in_list?(obj) && obj[position_column].to_i > position
264
+ } if @target
265
+ end
266
+
267
+ # This has the effect of moving all the higher items down one.
268
+ def increment_positions_on_higher_items(item)
269
+ return unless in_list?(item)
270
+ position = list_position(item)
271
+ connection.update(
272
+ "UPDATE #{join_table} SET #{position_column} = (#{position_column} + 1) " +
273
+ "WHERE #{foreign_key} = #{@owner.id} AND #{position_column} < #{position}"
274
+ )
275
+ @target.each { |obj|
276
+ obj[position_column] = obj[position_column].to_i + 1 if in_list?(obj) && obj[position_column].to_i < position
277
+ } if @target
278
+ end
279
+
280
+ # This has the effect of moving all the lower items down one.
281
+ def increment_positions_on_lower_items(position)
282
+ connection.update(
283
+ "UPDATE #{join_table} SET #{position_column} = (#{position_column} + 1) " +
284
+ "WHERE #{foreign_key} = #{@owner.id} AND #{position_column} >= #{position}"
285
+ )
286
+ @target.each { |obj|
287
+ obj[position_column] = obj[position_column].to_i + 1 if in_list?(obj) && obj[position_column].to_i >= position
288
+ } if @target
289
+ end
290
+
291
+ def increment_positions_on_all_items
292
+ connection.update(
293
+ "UPDATE #{join_table} SET #{position_column} = (#{position_column} + 1) " +
294
+ "WHERE #{foreign_key} = #{@owner.id}"
295
+ )
296
+ @target.each { |obj|
297
+ obj[position_column] = obj[position_column].to_i + 1 if in_list?(obj)
298
+ } if @target
299
+ end
300
+
301
+ def insert_at_position(item, position)
302
+ remove_from_list(item)
303
+ increment_positions_on_lower_items(position)
304
+ set_position(item, position)
305
+ end
306
+
307
+ # called after changing position values so the array reflects the updated ordering
308
+ def resort_array
309
+ @target.sort! {|x,y| x[position_column].to_i <=> y[position_column].to_i} if @target
310
+ end
311
+ end
312
+ end
313
+ end
314
+
315
+ ActiveRecord::Base.send(:include,RailsExtensions::HabtmList)
metadata ADDED
@@ -0,0 +1,58 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: webpulser-habtm_list
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - James Healy
8
+ - Cyril LEPAGNOT
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2009-08-16 00:00:00 -07:00
14
+ default_executable:
15
+ dependencies: []
16
+
17
+ description: Adds list-like position functionality to Rails has_and_belongs_to_many associations
18
+ email: cyril.lepagnot@webpulser.com
19
+ executables: []
20
+
21
+ extensions: []
22
+
23
+ extra_rdoc_files: []
24
+
25
+ files:
26
+ - README
27
+ - MIT-LICENSE
28
+ - Rakefile
29
+ - lib/webpulser-habtm_list.rb
30
+ has_rdoc: false
31
+ homepage: http://wiki.rubyonrails.org/rails/pages/BetterHabtmList
32
+ licenses:
33
+ post_install_message:
34
+ rdoc_options: []
35
+
36
+ require_paths:
37
+ - lib
38
+ required_ruby_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: "0"
43
+ version:
44
+ required_rubygems_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: "0"
49
+ version:
50
+ requirements: []
51
+
52
+ rubyforge_project:
53
+ rubygems_version: 1.3.5
54
+ signing_key:
55
+ specification_version: 2
56
+ summary: has_and_belongs_to_many list-like
57
+ test_files: []
58
+