pq-wsm 0.0.1

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.
Files changed (35) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +86 -0
  3. data/Rakefile +38 -0
  4. data/lib/generators/pc_queues/install_generator.rb +9 -0
  5. data/lib/generators/pc_queues/migration_generator.rb +15 -0
  6. data/lib/generators/pc_queues/templates/README +15 -0
  7. data/lib/generators/pc_queues/templates/initializer.rb +6 -0
  8. data/lib/generators/pc_queues/templates/migration.rb +36 -0
  9. data/lib/pc_queues.rb +43 -0
  10. data/lib/pc_queues/acts_as_enqueable.rb +31 -0
  11. data/lib/pc_queues/acts_as_queue_owner.rb +30 -0
  12. data/lib/pc_queues/priority_queue_item.rb +13 -0
  13. data/lib/pc_queues/queue.rb +312 -0
  14. data/lib/pc_queues/queue_item.rb +58 -0
  15. data/lib/pc_queues/queue_rule.rb +32 -0
  16. data/lib/pc_queues/queue_rule_set.rb +40 -0
  17. data/lib/pc_queues/queue_rules/boolean_queue_rule.rb +34 -0
  18. data/lib/pc_queues/queue_rules/numeric_queue_rule.rb +42 -0
  19. data/lib/pc_queues/queue_rules/sample_queue_rule.rb +36 -0
  20. data/lib/pc_queues/queue_rules/string_match_queue_rule.rb +44 -0
  21. data/lib/pc_queues/railtie.rb +10 -0
  22. data/lib/pc_queues/version.rb +3 -0
  23. data/lib/tasks/pc_queues_tasks.rake +4 -0
  24. data/spec/boolean_queue_rule_spec.rb +34 -0
  25. data/spec/numeric_queue_rule_spec.rb +123 -0
  26. data/spec/queue_spec.rb +494 -0
  27. data/spec/sample_queue_rule_spec.rb +34 -0
  28. data/spec/spec_helper.rb +30 -0
  29. data/spec/string_match_rule_spec.rb +76 -0
  30. data/spec/support/active_record.rb +42 -0
  31. data/spec/support/application.rb +122 -0
  32. data/spec/support/queue_helpers.rb +16 -0
  33. data/spec/support/redis_helpers.rb +32 -0
  34. data/spec/support/time.rb +23 -0
  35. metadata +131 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3c57d60c589e660e163c69281336d7df78c743c4
4
+ data.tar.gz: 2db6479616a377cd2d0b5e38390aa35463ba4983
5
+ SHA512:
6
+ metadata.gz: 57a65396a0b836c2c4380c11276b0789af30807abd8523b83ae91b24690208eaeec17396ed6ed4a686893dbc82fe38686e1f3fbaaab4f5b421c368fd04500b38
7
+ data.tar.gz: 37770ac8c7cbee10c60d8fedc599d5f3c492f424614c439c941ee6936fb087eb3232a1ed1589d7b57082a77afab1384c37491c264fd7a1fe4e06c34a3199b27a
data/README.md ADDED
@@ -0,0 +1,86 @@
1
+ ## PcQueues
2
+
3
+ A Queue Management Gem with Redis backing.
4
+
5
+ Copyright (c) ProctorCam Inc. 2014 All rights reserved
6
+
7
+ ### Installation
8
+
9
+ Dependency: Rails, Database, PubNub
10
+
11
+ Add the following to your Gemfile
12
+
13
+ ```ruby
14
+ gem 'pc-queues', :git => 'git@gitlab.proctorcam.com:proctorcam-development/pc-queues', :branch => 'develop'
15
+ ```
16
+
17
+ Run the install and migration generators
18
+
19
+ ```bash
20
+ > rails g pc_queues:install
21
+ > rails g pc_queues:migration
22
+ > rake db:migrate
23
+ ```
24
+
25
+ Update the initializer in config/initializers/pc_queues.rb to initialize the PubNub instance used.
26
+
27
+ That's it - you're done setting up.
28
+
29
+ ### Getting Started
30
+
31
+ Once you've got the gem setup. You'll designate Models as Enqueable and as Queue owners. Classes that are Enqueueable
32
+ can be enqueued and dequeued from a Queue Classes that are Queue owners can have has_one and has_many relationships to
33
+ Queues.
34
+
35
+ Queues are setup for single table inheritance, so you can derive from Queue to handle Class specific behavior.
36
+
37
+ An ActiveRecordClass can be both a Queue owner and Enqueable.
38
+
39
+ #### Setting Up to be a Queue Owner
40
+
41
+ ```ruby
42
+ require 'pc_queues'
43
+
44
+ class MyQueue < PcQueues::Queue
45
+ end
46
+
47
+ class MyModel < ActiveRecord::Base
48
+ include ACTS_AS_QUEUE_OWNER
49
+
50
+ has_many_queues_as :my_queues
51
+ has_one_queue_as :fast_stuff, class_name: 'MyQueue'
52
+ end
53
+ ```
54
+
55
+ #### Setting Up to be a Queue Item
56
+
57
+ ```ruby
58
+ require 'pc_queues'
59
+
60
+ class MyModel < ActiveRecord::Base
61
+ include ACTS_AS_ENQUEABLE
62
+
63
+ end
64
+ ```
65
+
66
+ #### Queue Manipulation
67
+
68
+ ```ruby
69
+ model = MyModel.create attr1: value1
70
+
71
+ # has_one
72
+ model.create_fast_stuff attr1: value1
73
+
74
+ # has_many
75
+ model.my_queues.create attr1: value1
76
+
77
+ # Enqueue/Dequeue
78
+ model.fast_stuff.enqueue my_acts_as_enqueuable_object
79
+
80
+ my_acts_as_enqueuable_object = model.fast_stuff.dequeue
81
+
82
+
83
+ # Queue Length
84
+ length = model.fast_stuff.length
85
+
86
+ ```
data/Rakefile ADDED
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+ begin
8
+ require 'rdoc/task'
9
+ rescue LoadError
10
+ require 'rdoc/rdoc'
11
+ require 'rake/rdoctask'
12
+ RDoc::Task = Rake::RDocTask
13
+ end
14
+
15
+ RDoc::Task.new(:rdoc) do |rdoc|
16
+ rdoc.rdoc_dir = 'rdoc'
17
+ rdoc.title = 'PcQueues'
18
+ rdoc.options << '--line-numbers'
19
+ rdoc.rdoc_files.include('README.rdoc')
20
+ rdoc.rdoc_files.include('lib/**/*.rb')
21
+ end
22
+
23
+
24
+
25
+
26
+ Bundler::GemHelper.install_tasks
27
+
28
+ require 'rake/testtask'
29
+
30
+ Rake::TestTask.new(:test) do |t|
31
+ t.libs << 'lib'
32
+ t.libs << 'test'
33
+ t.pattern = 'test/**/*_test.rb'
34
+ t.verbose = false
35
+ end
36
+
37
+
38
+ task :default => :test
@@ -0,0 +1,9 @@
1
+ class PcQueues::InstallGenerator < ::Rails::Generators::Base
2
+ source_root File.expand_path('../templates', __FILE__)
3
+ desc "Installs PcQueues."
4
+
5
+ def install
6
+ template "initializer.rb", "config/initializers/pc_queues.rb"
7
+ readme "README"
8
+ end
9
+ end
@@ -0,0 +1,15 @@
1
+ require 'rails/generators/active_record'
2
+
3
+ class PcQueues::MigrationGenerator < ::Rails::Generators::Base
4
+ include Rails::Generators::Migration
5
+ source_root File.expand_path('../templates', __FILE__)
6
+ desc "Installs PcQueues Gem migration file."
7
+
8
+ def install
9
+ migration_template 'migration.rb', 'db/migrate/create_pc_queues_tables.rb'
10
+ end
11
+
12
+ def self.next_migration_number(dirname)
13
+ ActiveRecord::Generators::Base.next_migration_number(dirname)
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ ===============================================================================
2
+
3
+ There is a setup that will be needed before you use pc-queues.
4
+
5
+ 1 - You'll need to edit config/initializers/pc_queues.rb to setup the PubNub instance
6
+ to use when publishing Queue activity and managing presence info.
7
+
8
+ 2 - Run the migration generator and migrate
9
+
10
+ > rails g pc_queues:migration
11
+ > rake db:migrate
12
+
13
+ That's it, that's all. Enjoy!
14
+
15
+ ===============================================================================
@@ -0,0 +1,6 @@
1
+ require 'pc_queues'
2
+
3
+ PcQueues.config do |config|
4
+ # PcQueues uses pubnub to update interested clients when things change
5
+ # config.the_pubnub = Pubnub::the_pubnub
6
+ end
@@ -0,0 +1,36 @@
1
+ class CreatePcQueuesTables < ActiveRecord::Migration
2
+ def change
3
+ create_table :queues do |t|
4
+ t.string :name
5
+ t.string :type
6
+ t.boolean :is_in_cold_start, :default => false
7
+ t.integer :queue_owner_id
8
+ t.string :queue_owner_type
9
+ end
10
+
11
+ create_table :queue_items do |t|
12
+ t.integer :position
13
+ t.integer :enqueued_time
14
+ t.integer :queue_id
15
+ t.integer :enqueable_id
16
+ t.string :enqueable_type
17
+ t.string :type
18
+ end
19
+
20
+ create_table :queue_rule_sets do |t|
21
+ t.boolean :is_any, :default => false
22
+ t.integer :queue_id
23
+ end
24
+
25
+ create_table :queue_rules do |t|
26
+ t.string :type
27
+ t.string :name
28
+ t.integer :numeric_value
29
+ t.string :string_value
30
+ t.integer :queue_rule_set_id
31
+ t.boolean :bool_value
32
+ end
33
+ end
34
+ end
35
+
36
+
data/lib/pc_queues.rb ADDED
@@ -0,0 +1,43 @@
1
+ #
2
+ # This source file is part of project: PcQueues
3
+ #
4
+ # A Proctoring Workflow Platform
5
+ #
6
+ # Copyright (c) ProctorCam Inc. 2014 All rights reserved
7
+ #
8
+
9
+ module PcQueues
10
+ autoload :ActsAsQueueOwner, "pc_queues/acts_as_queue_owner"
11
+ autoload :ActsAsEnqueable, "pc_queues/acts_as_enqueable"
12
+ autoload :Queue, "pc_queues/queue"
13
+ autoload :QueueRule, "pc_queues/queue_rule"
14
+ autoload :PriorityQueueItem, "pc_queues/priority_queue_item"
15
+ autoload :QueueItem, "pc_queues/queue_item"
16
+ autoload :QueueRuleSet, "pc_queues/queue_rule_set"
17
+
18
+ module QueueRules
19
+ autoload :BooleanQueueRule, "pc_queues/queue_rules/boolean_queue_rule"
20
+ autoload :NumericQueueRule, "pc_queues/queue_rules/numeric_queue_rule"
21
+ autoload :SampleQueueRule, "pc_queues/queue_rules/sample_queue_rule"
22
+ autoload :StringMatchQueueRule, "pc_queues/queue_rules/string_match_queue_rule"
23
+ end
24
+
25
+ class << self
26
+ ##
27
+ # Configuration for PcQueues - see config/initializers/pc_queues.rb for details
28
+ #
29
+ def config
30
+ yield self
31
+ end
32
+
33
+ def the_pubnub=(pubnub)
34
+ @@pubnub = pubnub
35
+ end
36
+
37
+ def the_pubnub
38
+ @@pubnub
39
+ end
40
+
41
+ end
42
+
43
+ end
@@ -0,0 +1,31 @@
1
+ #
2
+ # This source file is part of project: PcQueues
3
+ #
4
+ # A Proctoring Workflow Platform
5
+ #
6
+ # Copyright (c) ProctorCam Inc. 2014 All rights reserved
7
+ #
8
+ require 'active_support'
9
+
10
+ module PcQueues
11
+ module ActsAsEnqueable
12
+ extend ActiveSupport::Concern
13
+
14
+ included do
15
+ acts_as_enqueuable
16
+ end
17
+
18
+ module ClassMethods
19
+ def acts_as_enqueuable
20
+ # instances are enqueued with a polymorphic PcQueues::QueueItem instance
21
+ has_many :enqueued_with, :class_name => 'PcQueues::QueueItem', :as => :enqueable
22
+
23
+ # queues will return the PcQueues::Queue instances that
24
+ # this object is in.
25
+ has_many :queues, :through => :enqueued_with
26
+
27
+ before_destroy { |record| record.queues.each { |q| q.remove(record) } }
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,30 @@
1
+ #
2
+ # This source file is part of project: PcQueues
3
+ #
4
+ # A Proctoring Workflow Platform
5
+ #
6
+ # Copyright (c) ProctorCam Inc. 2014 All rights reserved
7
+ #
8
+ require 'active_support'
9
+
10
+ module PcQueues
11
+ module ActsAsQueueOwner
12
+ extend ActiveSupport::Concern
13
+
14
+ module ClassMethods
15
+ def has_many_queues_as(symbol, options = {})
16
+ klass = options[:class_name] or symbol.to_s.camelize.singularize
17
+ # has_many symbol, options.merge(:as => :queue_owner, :dependent => :destroy, :conditions => {:type => klass})
18
+ has_many symbol, -> {where(type: klass)}, options.merge(:as => :queue_owner, :dependent => :destroy) #==at :condtions is deprecated in rails 4, so replaced it with this
19
+ end
20
+
21
+ def has_one_queue_as(symbol, options = {})
22
+ klass = options[:class_name] or symbol.to_s.camelize.singularize
23
+ has_one symbol, -> {where(type: klass)}, options.merge(:as => :queue_owner, :dependent => :destroy)
24
+ # has_one symbol, options.merge(:as => :queue_owner, :dependent => :destroy, :conditions => {:type => klass})
25
+ end
26
+
27
+ end
28
+ end
29
+ end
30
+
@@ -0,0 +1,13 @@
1
+ #
2
+ # This source file is part of project: PcQueues
3
+ #
4
+ # A Proctoring Workflow Platform
5
+ #
6
+ # Copyright (c) ProctorCam Inc. 2014 All rights reserved
7
+ #
8
+ module PcQueues
9
+ # PriorityQueueItems are a flavor of QueueItems that
10
+ # have higher priority.
11
+ class PriorityQueueItem < QueueItem
12
+ end
13
+ end
@@ -0,0 +1,312 @@
1
+ #
2
+ # This source file is part of project: PcQueues
3
+ #
4
+ # A Proctoring Workflow Platform
5
+ #
6
+ # Copyright (c) ProctorCam Inc. 2014 All rights reserved
7
+ #
8
+ module PcQueues
9
+ # TODO: handle PubNub failures more gracefully
10
+
11
+ class Queue < ActiveRecord::Base
12
+ def self.accessible_attributes
13
+ [:name, :is_in_cold_start]
14
+ end
15
+ # attr_accessible :name, :is_in_cold_start
16
+
17
+ has_many :queue_items, dependent: :destroy, class_name: 'PcQueues::PriorityQueueItem'
18
+ has_many :priority_queue_items, dependent: :destroy, class_name: 'PcQueues::PriorityQueueItem'
19
+ has_many :queue_rule_sets, dependent: :destroy
20
+
21
+ belongs_to :queue_owner, :polymorphic => true
22
+
23
+ class_attribute :enqueue_callbacks, :dequeue_callbacks, :enqueable_type, :eager_load_relations
24
+
25
+ self.enqueue_callbacks = []
26
+ self.dequeue_callbacks = []
27
+ self.eager_load_relations = []
28
+
29
+ class << self
30
+
31
+ # An array of Queue Ids that have at least one client looking at them
32
+ def active_queue_ids
33
+ PubSub.proctors_for_class_instances(PcQueues::Queue).keys
34
+ end
35
+
36
+ # return Queue instances that have subscribers
37
+ def active_queues
38
+ Queue.find(active_queue_ids)
39
+ end
40
+
41
+ # Publish statistics for all "active" queues, where
42
+ # the length is non-zero and there are active subscribers.
43
+ #
44
+ # Returns the active queues
45
+ def publish_active_queues
46
+ aq = active_queues
47
+
48
+ aq.each do |queue|
49
+ PubSub.publish(
50
+ :channel => "pc-queue-#{queue.id}",
51
+ :message => {
52
+ :qualifier => "stats",
53
+ :data => queue.stats
54
+ }
55
+ ) { |data| }
56
+ end
57
+
58
+ aq
59
+ end
60
+
61
+ # Publish positions for all queue items in the queues provided
62
+ # if they have someone looking at them.
63
+ #
64
+ # The data published for each queue item is JSON-encoded:
65
+ # * position
66
+ # * estimated_time_left (in seconds)
67
+ #
68
+ def publish_positions(queues)
69
+ queues.each do |queue|
70
+ queue.publish_positions()
71
+ end
72
+ end
73
+
74
+ # Provide one or more callbacks to call before an item has been enqueued
75
+ #
76
+ # @param callbacks (symbol) the method name of the class that receives one
77
+ # parameters: the dequeued enqueable
78
+ def on_enqueue(*callbacks)
79
+ self.enqueue_callbacks = callbacks
80
+ end
81
+
82
+ # Provide one or more callbacks to call before an item has been dequeued
83
+ #
84
+ # @param callbacks (symbol) the method name of the class that receives one
85
+ # parameters: the dequeued enqueable
86
+ def on_dequeue(*callbacks)
87
+ self.dequeue_callbacks = callbacks
88
+ end
89
+
90
+ def enqueues(klass)
91
+ self.enqueable_type = klass
92
+ end
93
+
94
+ def eagerly_load(relations)
95
+ self.eager_load_relations = relations
96
+ end
97
+
98
+ end
99
+
100
+
101
+ # A list of all the elements in this queue
102
+ def enqueables(options = {})
103
+ @enqueables ||= queue_items.includes(:enqueable).where(options).map{ |queue_item| queue_item.enqueable }
104
+ end
105
+
106
+ def length()
107
+ # Due to Single Table Inheritance queue_items.count will count all types.
108
+ # This will include priority queue items as well.
109
+ queue_items.count
110
+ end
111
+
112
+
113
+ def priority_length()
114
+ priority_queue_items.count
115
+ end
116
+
117
+ # The current queue_item advancement rate
118
+ # Returns 0 if there is no item.
119
+ def rate()
120
+ first = queue_items.first
121
+ first and first.rate
122
+ end
123
+
124
+ # The current estimated wait time in seconds for items
125
+ # added to the queue. Returns 0 if there are no items.
126
+ def wait_time()
127
+ first = queue_items.first
128
+ first and first.wait_time
129
+ end
130
+
131
+ # Enqueue an Object onto the Queue
132
+ def enqueue(obj)
133
+ with_lock do
134
+ # queue_item = obj.enqueued_with.create enqueued_time: Time.now.to_i, position: (length + 1), :queue => self
135
+ queue_item = queue_items.create enqueued_time: Time.now.to_i, position: (length + 1), enqueable: obj
136
+
137
+ enqueue_queue_item queue_item
138
+ end
139
+
140
+ obj
141
+ end
142
+
143
+ # Enqueue an object onto the queue with high priority
144
+ # -> it will get dequeued before any enqueables that were
145
+ # added with `enqueue`
146
+ def priority_enqueue(obj)
147
+ with_lock do
148
+ queue_item = priority_queue_items.create enqueued_time: Time.now.to_i, position: (priority_length + 1), enqueable: obj
149
+
150
+ enqueue_queue_item queue_item
151
+ end
152
+
153
+ obj
154
+ end
155
+
156
+ # enqueue an object if it passes the rules for this queue
157
+ def enqueue_if_passes(enqueable, *args)
158
+ enqueue(enqueable) if rules_pass(enqueable, *args)
159
+ end
160
+
161
+ # Dequeue an Object from a Queue. The Object
162
+ # will no longer be in the Queue.
163
+ def dequeue()
164
+ obj = nil
165
+
166
+ while true do
167
+ begin
168
+ with_lock do
169
+ # Check priority_queue_items first, then queue_items
170
+ q_item = (priority_queue_items.first or queue_items.first)
171
+
172
+ return nil unless q_item
173
+
174
+ obj = q_item.enqueable
175
+
176
+ ActiveRecord::Associations::Preloader.new(obj, eager_load_relations).run
177
+
178
+ q_item.delete
179
+ end
180
+ rescue
181
+ # OK - Collided on a dequeued item, go try again.
182
+ next
183
+ end
184
+ # We've got an item, break
185
+ break
186
+ end
187
+
188
+ PubSub.publish(
189
+ :channel => "pc-queue-#{id}",
190
+ :message => [{
191
+ :qualifier => "dequeued",
192
+ :data => { :id => obj.id }
193
+ }, stats_message]
194
+ ) { |data| }
195
+
196
+ dequeue_callbacks.each { |callback| send callback, obj }
197
+
198
+ obj
199
+ end
200
+
201
+ # Remove a specific item from the Queue
202
+ def remove(obj)
203
+ begin
204
+ with_lock do
205
+ q_item = queue_items.where(enqueable_id: obj.id).first
206
+
207
+ return nil unless q_item
208
+
209
+ q_item.destroy
210
+ end
211
+ rescue
212
+ # Someone else beat us to the punch - just leave
213
+ return nil
214
+ end
215
+
216
+ PubSub.publish(
217
+ :channel => "pc-queue-#{id}",
218
+ :message => [{
219
+ :qualifier => "removed",
220
+ :data => { :id => obj.id }
221
+ }, stats_message ]
222
+ ) { |data| }
223
+
224
+ dequeue_callbacks.each { |callback| send callback, obj }
225
+
226
+ obj
227
+ end
228
+
229
+ # Determine if the rules for this queue pass
230
+ def rules_pass(enqueable, *args)
231
+ queue_rule_sets.includes(:queue_rules).all.each { |rule| return false unless rule.passes?(enqueable, *args) }
232
+ true
233
+ end
234
+
235
+ # Return the next object to be dequeued without dequeuing
236
+ def peek()
237
+ (item = queue_items.first)
238
+ item.cur_position = 1 unless item.nil?
239
+ item
240
+ end
241
+
242
+ # Current statistics for this queue
243
+ def stats
244
+ {
245
+ :length => length,
246
+ :rate => peek ? peek.rate : 0,
247
+ :longest_queue_wait => peek ? peek.wait_time : 0
248
+ }
249
+ end
250
+
251
+ # stats suitable for publishing
252
+ def stats_message
253
+ {
254
+ :qualifier => "stats",
255
+ :data => stats
256
+ }
257
+ end
258
+
259
+ def publish_stats
260
+ PcQueues::the_pubnub.publish(
261
+ :channel => "pc-queue-#{id}",
262
+ :message => stats_message
263
+ ) { |data| }
264
+ end
265
+
266
+ # Publish positions for this queue; see class method for more detail
267
+ def publish_positions
268
+ positionize()
269
+ queue_items.each do |item|
270
+ PubSub.publish(
271
+ :channel => "pc-queue-item-#{item.enqueable.id}",
272
+ :message => {
273
+ :qualifier => "stats",
274
+ :data => item.stats
275
+ }
276
+ ) { |data| }
277
+ end
278
+ end
279
+
280
+ protected
281
+
282
+ # DRY code for handling the common code between the regular and priority queues
283
+ def enqueue_queue_item(item)
284
+ # If validations fail, then the item is already in the queue.
285
+ return unless item.valid?
286
+
287
+ PubSub.publish(
288
+ :channel => "pc-queue-#{id}",
289
+ :message => [{
290
+ :qualifier => "enqueued",
291
+ :data => { :id => item.enqueable.id }
292
+ }, stats_message]
293
+ ) { |data| }
294
+
295
+ enqueue_callbacks.each { |callback| send callback, item.enqueable }
296
+ item.enqueable
297
+ end
298
+
299
+ # update the current position for Right Now of each queue item
300
+ def positionize
301
+ q_items = queue_items.select {|item| item.instance_of? ::PcQueues::QueueItem}
302
+ p_queue_items = queue_items.select {|item| item.instance_of? ::PcQueues::PriorityQueueItem}
303
+ (p_queue_items | q_items).each_with_index { |item, ndx| item.cur_position = ndx + 1 }
304
+ end
305
+
306
+ # Convert an array of objects to a hash by ids
307
+ def objects_to_id_hash(objects)
308
+ Hash[objects.collect {|o| [o.id, o]}]
309
+ end
310
+
311
+ end
312
+ end