acts_as_feedable 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc ADDED
@@ -0,0 +1,56 @@
1
+ This +acts_as+ extension allows objects to create feeds which describe them.
2
+ These feeds can then be used in a "Facebook-style" News Feed.
3
+
4
+ example:
5
+
6
+ class ItemUserTag < ActiveRecord::Base
7
+ acts_as_feedable :feed_name => 'Tag'
8
+ end
9
+
10
+ Feeds are created using the _with_feed wrappers around the save methods.
11
+
12
+ example:
13
+
14
+ annotation = Annotation.new
15
+ annotation.save_with_feed(current_user)
16
+
17
+ OR
18
+
19
+ annotation = annotation.find(params[:id])
20
+ annotation.update_attributes_with_feed(params[:annotation])
21
+
22
+ == Delegated Feeds
23
+
24
+ Sometimes a object that is created isn't the focus of the feed that represents it's creation. A
25
+ good example is a membership. When a membership is created we don't care about the membership itself
26
+ but about the user. When a membership is destroyed, we still want to reference the user in the feed about its creation.
27
+ This is solved by using the :delegate option to instead create a 'joined' feed with a User feedable in place of a 'created'
28
+ feed with a Membership feedable.
29
+
30
+ == Aggregate Feeds
31
+
32
+ Some things happen too frequently to list every occurance. Adding 50 items to a project shouldn't generate 50 feeds.
33
+ Passing :aggregate => true will instead create a single feed per person per day which counts the number of feedables created, updated, and destroyed.
34
+ Each feedable creation will also generate an aggregated_component, a link to the object which is aggregated into the feed. This allows feeds
35
+ to show each individual object which was added or destroyed upon request.
36
+
37
+ == Deleting a Feedable
38
+
39
+ When a feedable is deleted, one of three things happen:
40
+ * If the feedable is being aggregated, we can add an aggregated component which represents the destruction of the feedable.
41
+
42
+ If the feedable is not being aggregated, there are two options:
43
+ * We simply delete all related feeds to avoid problems that would occur because feed permissions are proxies of the destroyed object.
44
+ * If the +keep_feeds_when_destroyed+ flag is set, we can add a destruction feed and inherit permissions from the feed's scoping object instead.
45
+
46
+ == Assumptions
47
+
48
+ * The acts_as_permissable plugin is being used by the application.
49
+ * Feedables being aggregated all have the same permissions.
50
+ * Feedables being aggregated are all public or permissable_proxies of the object they reference
51
+
52
+ == License
53
+
54
+ ActsAsFeeable is released under the MIT license:
55
+
56
+ * http://www.opensource.org/licenses/MIT
@@ -0,0 +1,20 @@
1
+ class Feed < ActiveRecord::Base
2
+ belongs_to :initiator, :class_name => 'User'
3
+
4
+ belongs_to :feedable, :polymorphic => true
5
+ belongs_to :scoping_object, :polymorphic => true
6
+
7
+ has_many :feed_aggregated_components, :dependent => :destroy, :order => 'feed_aggregated_components.updated_at DESC'
8
+
9
+ default_scope order('feeds.updated_at DESC')
10
+
11
+ # Used to group feeds by the day they occurred
12
+ def date
13
+ updated_at.to_date.to_formatted_s(:long_ordinal)
14
+ end
15
+
16
+ # Is this an aggregate feed
17
+ def aggregate?
18
+ added_count > 0 || updated_count > 0 || removed_count > 0
19
+ end
20
+ end
@@ -0,0 +1,32 @@
1
+ class FeedAggregatedComponent < ActiveRecord::Base
2
+ belongs_to :feed
3
+ belongs_to :reference, :polymorphic => true
4
+ belongs_to :secondary_reference, :polymorphic => true
5
+
6
+ def self.created_today(feed, reference, secondary_reference = nil)
7
+ time_scope('created', Date.today, feed, reference, secondary_reference)
8
+ end
9
+
10
+ def self.created_recently(feed, reference, secondary_reference = nil)
11
+ time_scope('created', Time.now - 5.minutes, feed, reference, secondary_reference)
12
+ end
13
+
14
+ def self.updated_today(feed, reference, secondary_reference = nil)
15
+ time_scope('updated', Date.today, feed, reference, secondary_reference)
16
+ end
17
+
18
+ def self.destroyed_today(feed, reference, secondary_reference = nil)
19
+ time_scope('destroyed', Date.today, feed, reference, secondary_reference)
20
+ end
21
+
22
+ def self.time_scope(action, time, feed, reference, secondary_reference)
23
+ scope = where(:action => action, :feed_id => feed.id, :reference_type => reference.class.to_s, :reference_id => reference.id)
24
+ scope = scope.where("created_at > ?", time)
25
+
26
+ if secondary_reference.present?
27
+ scope = scope.where(:secondary_reference_type => secondary_reference.class.to_s, :secondary_reference_id => secondary_reference.id)
28
+ end
29
+
30
+ return scope.first
31
+ end
32
+ end
@@ -0,0 +1,18 @@
1
+ require 'feedable/acts_as_feedable'
2
+
3
+ module ActsAsFeedable
4
+ class Engine < Rails::Engine
5
+ initializer "acts_as_feedable.init" do
6
+ ActiveRecord::Base.send :extend, Feedable::ActsAsFeedable::ActMethod
7
+ end
8
+
9
+ config.to_prepare do
10
+ if defined?(ActsAsJoinable::Engine)
11
+ require 'feedable/joinable_extensions'
12
+ JoinableExtensions.add
13
+ else
14
+ puts "[ActsAsFeedable] ActsAsJoinable not loaded. Skipping extensions."
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,376 @@
1
+ module Feedable #:nodoc:
2
+ module ActsAsFeedable #:nodoc:
3
+ module ActMethod
4
+ # Configuration options are:
5
+ #
6
+ # * +keep_feeds_when_destroyed+ - specifies whether to keep the feeds when a feedable is destroyed. This should only be done if the feedable is public or is scoped to another feedable.
7
+ # * +target_name+ - specifies how to get the name of the target (used by the view to store the name of the primary object the feed links to). (default is nil)
8
+ # * +scoping_object?+ - Boolean - If true, this object will become the scoping object for all feedables descending from it. eg. a Project is a scoping object for all discussions, comments, and writeboards within the project.
9
+ # * +parent+ - Specifies the code to execute to traverse up the feedable chain in search of any scoping objects
10
+ #
11
+ # === Delegate Options
12
+ #
13
+ # * +actions+ - overrides the :created, :updated, and :destroyed actions with custom actions (must be set if the +delegate+ option is used)
14
+ # * +references+ - provides feeds with a surrogate feedable when the object itself isn't the focus of the feed. (must be set if the +delegate+ option is used)
15
+ #
16
+ # === Aggregate Options
17
+ #
18
+ # * +action+ - specifies the action group the aggregate feed should belong to (must be set if the +aggregate+ option is used)
19
+ # * +references+ - provides aggregate feeds with a feedable by which to group this object's feeds. (must be set if the +aggregate+ option is used)
20
+ # * +component_reference+ - specifies the object that is referenced by the aggregated feed component. This needs to be the object that the feed "happens to" (e.g. a label is applied to an item). We use this when determining how to handle subsequent update and destroy actions. (aggregated feed components are used to store each action that is referenced by an aggregate feed) (default is self)
21
+ # * +component_secondary_reference+ - specifies an optional secondary object that is referenced by an aggregated feed component. For example, the item that a label is being applied to (default is nil)
22
+ # * +component_reference_name+ - specifies how to get the name of the reference (used when a component is destroyed). (default is component_reference.name)
23
+ # * +component_secondary_reference_name+ - specifies how to get the name of the secondary_reference (used when the secondary_reference is destroyed). (default is component_secondary_reference.name)
24
+ def acts_as_feedable(options = {})
25
+ extend ClassMethods unless (class << self; included_modules; end).include?(ClassMethods)
26
+ include InstanceMethods unless included_modules.include?(InstanceMethods)
27
+
28
+ # Sanity Check
29
+ options.assert_valid_keys(:scoping_object?, :parent, :keep_feeds_when_destroyed, :target_name, :delegate, :aggregate)
30
+
31
+ raise 'target_name option must be set if the keep_feeds_when_destroyed option is used' if options.key?(:keep_feeds_when_destroyed) && !options.key?(:target_name)
32
+
33
+ options[:delegate].assert_valid_keys(:actions, :references) if options[:delegate].present?
34
+ raise 'actions option must be set if the delegate option is used' if options[:delegate].is_a?(Hash) && options[:delegate][:actions].blank?
35
+ raise 'references option must be set if the delegate option is used' if options[:delegate].is_a?(Hash) && options[:delegate][:references].blank?
36
+
37
+ options[:aggregate].assert_valid_keys(:action, :references, :component_reference, :component_secondary_reference, :component_reference_name, :component_secondary_reference_name) if options[:aggregate].present?
38
+ raise 'action option must be set if the aggregate option is used' if options[:aggregate].is_a?(Hash) && options[:aggregate][:action].blank?
39
+ raise 'references option must be set if the aggregate option is used' if options[:aggregate].is_a?(Hash) && options[:aggregate][:references].blank?
40
+
41
+ options.reverse_merge!(:keep_feeds_when_destroyed => false, :delegate => {}, :aggregate => {})
42
+
43
+ self.feed_options = options
44
+
45
+ class_eval <<-EOV
46
+
47
+ def keep_feeds_when_destroyed?
48
+ #{options[:keep_feeds_when_destroyed]}
49
+ end
50
+
51
+ def feedable
52
+ if delegating?
53
+ #{options[:delegate][:references]}
54
+ elsif aggregating?
55
+ #{options[:aggregate][:references]}
56
+ else
57
+ self
58
+ end
59
+ end
60
+
61
+ def target_name
62
+ #{options[:target_name] || 'nil'}
63
+ end
64
+
65
+ def parent_feedable
66
+ #{options[:parent] || 'nil'}
67
+ end
68
+
69
+ # Aggregate Feed Methods
70
+
71
+ def component_reference
72
+ #{options[:aggregate][:component_reference] || 'self'}
73
+ end
74
+
75
+ def component_reference_name
76
+ #{options[:aggregate][:component_reference_name] || 'component_reference.name'}
77
+ end
78
+
79
+ def component_secondary_reference
80
+ #{options[:aggregate][:component_secondary_reference] || 'nil'}
81
+ end
82
+
83
+ def component_secondary_reference_name
84
+ #{options[:aggregate][:component_secondary_reference_name] || 'component_secondary_reference.try(:name)'}
85
+ end
86
+ # END Aggregate Feed Methods
87
+ EOV
88
+
89
+ end
90
+ end
91
+
92
+ module ClassMethods
93
+ def self.extended(base)
94
+ base.after_create :add_created_feed
95
+ base.after_update :add_updated_feed
96
+ base.before_destroy :setup_destroyed_feed_if_keeping_feeds
97
+ base.after_destroy :add_destroyed_feed, :destroy_scoped_feeds
98
+
99
+ base.cattr_accessor :feed_options
100
+ base.has_many :feeds, :as => :feedable
101
+ end
102
+
103
+ def create_with_feed(user, *args)
104
+ options = args.extract_options!
105
+ options.merge!(:feed_initiator_id => user.id)
106
+ return create(options)
107
+ end
108
+ end
109
+
110
+ module InstanceMethods
111
+ attr_accessor :feed_initiator_id
112
+
113
+ def acts_like_feedable?
114
+ true
115
+ end
116
+
117
+ # Returns the scoping object for this object
118
+ def scoping_object
119
+ scoping_ancestor || parent_feedable
120
+ end
121
+
122
+ # Returns true if this object is a scoping object
123
+ def scoping_object?
124
+ self.feed_options[:scoping_object?] == true
125
+ end
126
+
127
+ def delegating?
128
+ self.feed_options[:delegate][:actions].present? && self.feed_options[:delegate][:references].present?
129
+ end
130
+
131
+ def delegate_action_for(action)
132
+ self.feed_options[:delegate][:actions][action.to_sym] || action.to_s
133
+ end
134
+
135
+ def aggregating?
136
+ self.feed_options[:aggregate][:action].present? && self.feed_options[:aggregate][:references].present?
137
+ end
138
+
139
+ def aggregate_action
140
+ self.feed_options[:aggregate][:action]
141
+ end
142
+ # ActiveRecord Wrappers with initiators
143
+
144
+ def save_with_feed(user, *args)
145
+ self.feed_initiator_id = user.id
146
+ return save(*args)
147
+ end
148
+
149
+ def save_with_feed!(user, *args)
150
+ self.feed_initiator_id = user.id
151
+ return save!(*args)
152
+ end
153
+
154
+ def update_attributes_with_feed(user, *args)
155
+ self.feed_initiator_id = user.id
156
+ return update_attributes(*args)
157
+ end
158
+
159
+ def update_attributes_with_feed!(user, *args)
160
+ self.feed_initiator_id = user.id
161
+ return update_attributes(*args)
162
+ end
163
+
164
+ def destroy_with_feed(user, *args)
165
+ self.feed_initiator_id = user.id
166
+ return destroy(*args)
167
+ end
168
+
169
+ def with_feed(user)
170
+ self.feed_initiator_id = user.id
171
+ return self
172
+ end
173
+
174
+ # END ActiveRecord Wrappers with initiators
175
+
176
+ # Adds a custom feed for this object with the given +action+ and +initiator+
177
+ def add_custom_feed(action, initiator, options = {})
178
+ feed = Feed.new(:initiator => initiator, :action => action, :scoping_object => scoping_object, :feedable => self, :target_name => target_name)
179
+
180
+ feed.initial_instance_level_permission_map = options[:map] if options[:map]
181
+
182
+ feed.save!
183
+ end
184
+
185
+ private
186
+
187
+ # Searches up the chain of parent_feedables until it hits a scoping object and returns it
188
+ # If none is found, returns nil
189
+ def scoping_ancestor
190
+ if parent_feedable && parent_feedable.acts_like?(:feedable)
191
+ if parent_feedable.scoping_object?
192
+ parent_feedable
193
+ else
194
+ parent_feedable.send(:scoping_ancestor)
195
+ end
196
+ end
197
+ end
198
+
199
+ # Creates a feed about the creation of the feedable
200
+ def add_created_feed
201
+ return unless feed_initiator_id
202
+
203
+ if aggregating?
204
+ update_aggregate_feed(:added)
205
+ elsif delegating?
206
+ create_feed_with_defaults(:action => delegate_action_for('created'))
207
+ else
208
+ create_feed_with_defaults(:action => 'created')
209
+ end
210
+ clear_initiator
211
+ end
212
+
213
+ # Creates a feed about the update of the feedable
214
+ def add_updated_feed
215
+ return unless feed_initiator_id
216
+
217
+ if aggregating?
218
+ update_aggregate_feed(:updated)
219
+ elsif delegating?
220
+ create_feed_with_defaults(:action => delegate_action_for('updated'))
221
+ else
222
+ create_feed_with_defaults(:action => 'updated')
223
+ end
224
+ clear_initiator
225
+ end
226
+
227
+ # Creates a feed about the deletion of the feedable.
228
+ # If the feed isn't aggregated then it deletes all existing feeds related to that object.
229
+ def add_destroyed_feed
230
+ if aggregating?
231
+ update_aggregate_feed(:removed) if feed_initiator_id
232
+ elsif delegating?
233
+ create_feed_with_defaults(:action => delegate_action_for('destroyed')) if feed_initiator_id
234
+ elsif !keep_feeds_when_destroyed?
235
+ Feed.destroy_all(:feedable_type => self.class.to_s, :feedable_id => id)
236
+ end
237
+ clear_initiator
238
+ end
239
+
240
+ # Create the destroyed feed before the feedable is destroyed so it gets the correct permission mappings
241
+ def setup_destroyed_feed_if_keeping_feeds
242
+ if keep_feeds_when_destroyed? && feed_initiator_id
243
+ create_feed_with_defaults(:action => 'destroyed')
244
+ end
245
+ end
246
+
247
+ # Destroy all feeds which are scoped to the feedable.
248
+ # This will prevent feeds from not rendering because the feedable has been destroyed.
249
+ def destroy_scoped_feeds
250
+ unless aggregating?
251
+ Feed.destroy_all(:scoping_object_type => self.class.to_s, :scoping_object_id => id)
252
+ end
253
+ end
254
+
255
+ def create_feed_with_defaults(options)
256
+ Feed.create(options.merge(:initiator_id => feed_initiator_id, :scoping_object => scoping_object, :feedable => feedable, :target_name => target_name))
257
+ end
258
+
259
+ # Called when the feedable generates aggregate feeds.
260
+ #
261
+ # Increments one of the counts depending on whether the feedable is created or destroyed
262
+ # and creates an FeedAggregatedComponent to represent the feedable.
263
+ #
264
+ # eg. When a label is created update the count in the project_label_created feed and create a FeedAggregatedComponent which
265
+ # points to the label.
266
+ def update_aggregate_feed(direction)
267
+ feed = find_existing_aggregate_feed || create_aggregate_feed
268
+
269
+ if direction.eql?(:added)
270
+ aggregated_component_addition(feed)
271
+ elsif direction.eql?(:updated)
272
+ aggregated_component_update(feed)
273
+ else
274
+ aggregated_component_removal(feed)
275
+ end
276
+ end
277
+
278
+ def aggregated_component_addition(feed)
279
+ # If the aggregated component was already removed today, just remove the destroyed
280
+ # components to zero out the feed.
281
+ #
282
+ # Else add a created FeedAggregatedComponent instead
283
+ if remove_todays_destroyed_feed(feed, component_reference)
284
+ # Get rid of the feed completely if there are no more FeedAggregatedComponents
285
+ feed.destroy if feed.added_count == 0 && feed.updated_count == 0 && feed.removed_count == 0
286
+ else
287
+ feed.increment!(:added_count)
288
+ create_component(feed, 'created')
289
+ end
290
+ end
291
+
292
+ def aggregated_component_update(feed)
293
+ # Only add an 'updated' FeedAggregatedComponent if a matching FeedAggregatedComponent wasn't created lately or updated today.
294
+ unless FeedAggregatedComponent.created_recently(feed, component_reference, component_secondary_reference) || FeedAggregatedComponent.updated_today(feed, component_reference, component_secondary_reference)
295
+ feed.increment!(:updated_count)
296
+ create_component(feed, 'updated')
297
+ end
298
+ end
299
+
300
+ def aggregated_component_removal(feed)
301
+ # If the aggregated component was already created today, just remove the created
302
+ # and updated components to zero out the feed.
303
+ #
304
+ # Else get rid of any updated FeedAggregatedComponents from today and add a destroyed
305
+ # FeedAggregatedComponent instead
306
+ remove_todays_updated_feed(feed, component_reference)
307
+ if remove_todays_created_feed(feed, component_reference)
308
+ # Get rid of the feed completely if there are no more FeedAggregatedComponents
309
+ feed.destroy if feed.added_count == 0 && feed.updated_count == 0 && feed.removed_count == 0
310
+ else
311
+ feed.increment!(:removed_count)
312
+ create_component(feed, 'destroyed')
313
+ end
314
+ end
315
+
316
+ # Remove a 'created' FeedAggregatedComponent that was created today for the provided
317
+ # *component_reference* and return true if it was removed
318
+ def remove_todays_created_feed(feed, component_reference)
319
+ if feedable_aggregated_component = FeedAggregatedComponent.created_today(feed, component_reference, component_secondary_reference)
320
+ feed.decrement!(:added_count)
321
+ feedable_aggregated_component.destroy
322
+
323
+ return true
324
+ end
325
+ end
326
+
327
+ # Remove an 'updated' FeedAggregatedComponent that was created today for the provided
328
+ # *component_reference* and return true if it was removed
329
+ def remove_todays_updated_feed(feed, component_reference)
330
+ if feedable_aggregated_component = FeedAggregatedComponent.updated_today(feed, component_reference, component_secondary_reference)
331
+ feed.decrement!(:updated_count)
332
+ feedable_aggregated_component.destroy
333
+
334
+ return true
335
+ end
336
+ end
337
+
338
+ # Remove a 'destroyed' FeedAggregatedComponent that was created today for the provided
339
+ # *component_reference* and return true if it was removed
340
+ def remove_todays_destroyed_feed(feed, component_reference)
341
+ if feedable_aggregated_component = FeedAggregatedComponent.destroyed_today(feed, component_reference, component_secondary_reference)
342
+ feed.decrement!(:removed_count)
343
+ feedable_aggregated_component.destroy
344
+
345
+ return true
346
+ end
347
+ end
348
+
349
+ # Find an existing aggregate feed which was created on *date*
350
+ def find_existing_aggregate_feed(date = Date.today)
351
+ Feed.where("initiator_id = ? AND feedable_type = ? AND feedable_id = ? AND action = ? AND created_at > ? AND created_at < ?", feed_initiator_id, feedable.class.to_s, feedable.id, aggregate_action, date, date + 1.day).first
352
+ end
353
+
354
+ def create_aggregate_feed
355
+ aggregate_feed = Feed.new(:initiator_id => feed_initiator_id, :scoping_object => scoping_object, :feedable => feedable, :target_name => target_name, :action => aggregate_action)
356
+
357
+ # Change permission mapping to respect permissions of object being aggregated.
358
+ aggregate_feed.view_permission = self.view_permission if acts_like?(:joinable_component)
359
+
360
+ aggregate_feed.save!
361
+
362
+ return aggregate_feed
363
+ end
364
+
365
+ # Creates a component to reflect the *action* of the feedable.
366
+ def create_component(feed, action)
367
+ FeedAggregatedComponent.create(:action => action, :feed => feed, :reference => component_reference, :reference_name => component_reference_name, :secondary_reference => component_secondary_reference, :secondary_reference_name => component_secondary_reference_name)
368
+ end
369
+
370
+ # Clears the initiator id from the feedable
371
+ def clear_initiator
372
+ self.feed_initiator_id = nil
373
+ end
374
+ end
375
+ end
376
+ end
@@ -0,0 +1,80 @@
1
+ module Joinable::ActsAsJoinable::ClassMethods
2
+ class << self
3
+ alias_method :extended_without_feedable, :extended
4
+
5
+ def extended(base)
6
+ extended_without_feedable(base)
7
+ base.before_validation :dont_create_membership_invitation_feeds, :on => :create
8
+ end
9
+ end
10
+ end
11
+
12
+ module Joinable::ActsAsJoinable::InstanceMethods
13
+ # Don't create feeds for membership invitations when the joinable itself is being created
14
+ def dont_create_membership_invitation_feeds
15
+ membership_invitations.each { |invitation| invitation.no_default_feed = true }
16
+ end
17
+ end
18
+
19
+ module JoinableExtensions
20
+ def self.add
21
+ extend_membership
22
+ extend_membership_invitation
23
+ extend_membership_request
24
+ end
25
+
26
+ def self.extend_membership
27
+ Membership.class_eval do
28
+ acts_as_feedable :parent => 'joinable', :delegate => {:references => 'user', :actions => {:created => 'joined', :destroyed => 'left'}}
29
+
30
+ private
31
+
32
+ before_destroy :ensure_feed_creation
33
+
34
+ def ensure_feed_creation
35
+ with_feed(initiator) if initiator.present?
36
+ end
37
+ end
38
+ end
39
+
40
+ def self.extend_membership_invitation
41
+ MembershipInvitation.class_eval do
42
+ acts_as_feedable :parent => 'joinable', :delegate => {:references => 'user', :actions => {:created => 'invited', :destroyed => 'cancelled_invite'}}
43
+
44
+ attr_accessor :no_default_feed
45
+
46
+ private
47
+
48
+ def create_associated_membership_on_accept(current_user)
49
+ self.no_default_feed = true # Default feed has incorrect initiator. We're about to create a feed with the correct initiator.
50
+ Membership.create_with_feed(user, :joinable => joinable, :user => user, :permissions => permissions)
51
+ end
52
+
53
+ def destroy_self_on_decline(current_user)
54
+ self.no_default_feed = true # Default feed has incorrect initiator. We're about to create a feed with the correct initiator.
55
+ destroy_with_feed(user)
56
+ end
57
+
58
+ before_create :ensure_feed_creation
59
+ before_destroy :ensure_feed_creation
60
+
61
+ # Don't create a destroyed feed if the user is accepting a membership invitation or a membership request exists for this User.
62
+ # In that case, a membership was created, and this invitation should be 'invisible' to the Users and the UI. Thus no feeds should be created.
63
+ def ensure_feed_creation
64
+ with_feed(initiator) unless no_default_feed || joinable.membership_for?(user) || joinable.membership_request_for?(user)
65
+ end
66
+ end
67
+ end
68
+
69
+ def self.extend_membership_request
70
+ MembershipRequest.class_eval do
71
+ acts_as_feedable :parent => 'joinable', :delegate => {:references => 'user', :actions => {:created => 'requested', :destroyed => 'cancelled_request'}}
72
+
73
+ private
74
+
75
+ def create_associated_membership_on_grant(current_user, permissions)
76
+ Membership.create_with_feed(current_user, :joinable => joinable, :user => user, :permissions => permissions)
77
+ end
78
+ end
79
+ end
80
+ end
metadata ADDED
@@ -0,0 +1,52 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: acts_as_feedable
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Ryan Wallace
9
+ - Nicholas Jakobsen
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2012-04-30 00:00:00.000000000 Z
14
+ dependencies: []
15
+ description: Allows objects to create feeds which describe them. These feeds can then
16
+ be used in a "Facebook-style" News Feed.
17
+ email: technical@rrnpilot.org
18
+ executables: []
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - app/models/feed.rb
23
+ - app/models/feed_aggregated_component.rb
24
+ - lib/acts_as_feedable.rb
25
+ - lib/feedable/acts_as_feedable.rb
26
+ - lib/feedable/joinable_extensions.rb
27
+ - README.rdoc
28
+ homepage: http://github.com/rrn/acts_as_feedable
29
+ licenses: []
30
+ post_install_message:
31
+ rdoc_options: []
32
+ require_paths:
33
+ - lib
34
+ required_ruby_version: !ruby/object:Gem::Requirement
35
+ none: false
36
+ requirements:
37
+ - - ! '>='
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ required_rubygems_version: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ requirements: []
47
+ rubyforge_project:
48
+ rubygems_version: 1.8.25
49
+ signing_key:
50
+ specification_version: 3
51
+ summary: Allows objects to create feeds which describe them
52
+ test_files: []