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 +56 -0
- data/app/models/feed.rb +20 -0
- data/app/models/feed_aggregated_component.rb +32 -0
- data/lib/acts_as_feedable.rb +18 -0
- data/lib/feedable/acts_as_feedable.rb +376 -0
- data/lib/feedable/joinable_extensions.rb +80 -0
- metadata +52 -0
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
|
data/app/models/feed.rb
ADDED
@@ -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: []
|