activr 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,441 @@
1
+ module Activr
2
+
3
+ #
4
+ # With a timeline you can create complex activity feeds.
5
+ #
6
+ # When creating a {Timeline} class you specify:
7
+ #
8
+ # - what model in your application owns that timeline: the `recipient`
9
+ # - what activities will be displayed in that timeline: the `routes`
10
+ #
11
+ # Routes can be resolved thanks to:
12
+ #
13
+ # - a predefined routing declared with routing method, then specified in the :using route setting
14
+ # - an activity path specified in the :to route setting
15
+ # - a call on timeline class method specified in the :using route setting
16
+ #
17
+ # @example For example, this is a user newsfeed timeline
18
+ #
19
+ # class UserNewsFeedTimeline < Activr::Timeline
20
+ # # that timeline is for users
21
+ # recipient User
22
+ #
23
+ # # this is a predefined routing, to fetch all followers of an activity actor
24
+ # routing :actor_follower, :to => Proc.new{ |activity| activity.actor.followers }
25
+ #
26
+ # # define a routing with a class method, to fetch all followers of an activity album
27
+ # def self.album_follower(activity)
28
+ # activity.album.followers
29
+ # end
30
+ #
31
+ # # predefined routing: users will see in their news feed when a friend they follow likes a picture
32
+ # route LikePictureActivity, :using => :actor_follower
33
+ #
34
+ # # activity path: users will see in their news feed when someone adds a picture in one of their albums
35
+ # route AddPictureActivity, :to => 'album.owner', :humanize => "{{{actor}}} added a picture to your album {{{album}}}"
36
+ #
37
+ # # method call: users will see in their news feed when someone adds a picture in an album they follow
38
+ # route AddPictureActivity, :using => :album_follower
39
+ #
40
+ # end
41
+ #
42
+ # When an activity is routed to a timeline, a Timeline Entry is stored in database and that Timeline Entry contains
43
+ # a copy of the original activity: so Activr uses a "Fanout on write" mecanism to dispatch activities to timelines.
44
+ #
45
+ # Several callbacks are invoked on timeline instance during the activity handling workflow:
46
+ #
47
+ # - .should_route_activity? - Returns `false` to skip activity routing
48
+ # - #should_handle_activity? - Returns `false` to skip routed activity
49
+ # - #should_store_timeline_entry? - Returns `false` to cancel timeline entry storing
50
+ # - #will_store_timeline_entry - This is your last chance to modify timeline entry before it is stored
51
+ # - #did_store_timeline_entry - Called just after timeline entry was stored
52
+ #
53
+ class Timeline
54
+
55
+ autoload :Entry, 'activr/timeline/entry'
56
+ autoload :Route, 'activr/timeline/route'
57
+
58
+
59
+ # Recipient class
60
+ class_attribute :recipient_class, :instance_writer => false
61
+ self.recipient_class = nil
62
+
63
+ # Maximum length (0 means 'no limit')
64
+ class_attribute :trim_max_length, :instance_writer => false
65
+ self.trim_max_length = 0
66
+
67
+ # Predefined routings
68
+ class_attribute :routings, :instance_writer => false
69
+ self.routings = { }
70
+
71
+ # Routes (ordered by priority)
72
+ class_attribute :routes, :instance_writer => false
73
+ self.routes = [ ]
74
+
75
+
76
+ class << self
77
+
78
+ # Get timeline class kind
79
+ #
80
+ # @example
81
+ # UserNewsFeedTimeline.kind
82
+ # # => 'user_news_feed'
83
+ #
84
+ # @note Kind is inferred from Class name, unless `#set_kind` method is used to force a custom value
85
+ #
86
+ # @return [String] Kind
87
+ def kind
88
+ @kind ||= @forced_kind || Activr::Utils.kind_for_class(self, 'timeline')
89
+ end
90
+
91
+ # Set timeline kind
92
+ #
93
+ # @note Default kind is inferred from class name
94
+ #
95
+ # @param forced_kind [String] Timeline kind
96
+ def set_kind(forced_kind)
97
+ @forced_kind = forced_kind.to_s
98
+ end
99
+
100
+ # Get route defined with given kind
101
+ #
102
+ # @param route_kind [String] Route kind
103
+ # @return [Timeline::Route] Corresponding Route instance
104
+ def route_for_kind(route_kind)
105
+ self.routes.find do |defined_route|
106
+ (defined_route.kind == route_kind)
107
+ end
108
+ end
109
+
110
+ # Get route defined with given kind
111
+ #
112
+ # @param routing_kind [String] Routing kind
113
+ # @param activity_class [Class] Activity class
114
+ # @return [Timeline::Route] Corresponding Route instance
115
+ def route_for_routing_and_activity(routing_kind, activity_class)
116
+ self.route_for_kind(Activr::Timeline::Route.kind_for_routing_and_activity(routing_kind, activity_class.kind))
117
+ end
118
+
119
+ # Get all routes defined for given activity
120
+ #
121
+ # @param activity [Activity] Activity instance
122
+ # @return [Array<Timeline::Route>] List of Route instances
123
+ def routes_for_activity(activity_class)
124
+ self.routes.find_all do |defined_route|
125
+ (defined_route.activity_class == activity_class)
126
+ end
127
+ end
128
+
129
+ # Check if given route was already defined
130
+ #
131
+ # @param route_to_check [Timeline::Route] Route to check
132
+ # @return [true, false]
133
+ def have_route?(route_to_check)
134
+ (route_to_check.timeline_class == self) && !self.route_for_kind(route_to_check.kind).blank?
135
+ end
136
+
137
+ # Callback: just before trying to route given activity
138
+ #
139
+ # @note MAY be overriden by child class
140
+ #
141
+ # @param activity [Activity] Activity to route
142
+ # @return [true,false] `false` to skip activity
143
+ def should_route_activity?(activity)
144
+ true
145
+ end
146
+
147
+ # Is it a valid recipient
148
+ #
149
+ # @param recipient [Object] Recipient to check
150
+ # @return [true, false]
151
+ def valid_recipient?(recipient)
152
+ (self.recipient_class && recipient.is_a?(self.recipient_class)) || Activr.storage.valid_id?(recipient)
153
+ end
154
+
155
+ # Get recipient id for given recipient
156
+ #
157
+ # @param recipient [Object] Recipient
158
+ # @return [Object] Recipient id
159
+ def recipient_id(recipient)
160
+ if self.recipient_class && recipient.is_a?(self.recipient_class)
161
+ recipient.id
162
+ elsif Activr.storage.valid_id?(recipient)
163
+ recipient
164
+ else
165
+ raise "Invalid recipient #{recipient.inspect} for timeline #{self}"
166
+ end
167
+ end
168
+
169
+
170
+ #
171
+ # Class interface
172
+ #
173
+
174
+ # Set recipient class
175
+ #
176
+ # @example Several instance methods are injected in given `klass`, for example with timeline:
177
+ #
178
+ # class UserNewsFeedTimeline < Activr::Timeline
179
+ # recipient User
180
+ #
181
+ # # ...
182
+ # end
183
+ #
184
+ # @example Those methods are created:
185
+ #
186
+ # class User
187
+ # # fetch latest timeline entries
188
+ # def user_news(limit, options = { })
189
+ # # ...
190
+ # end
191
+ #
192
+ # # get total number of timeline entries
193
+ # def user_news_count
194
+ # # ...
195
+ # end
196
+ # end
197
+ #
198
+ # @param klass [Class] Recipient class
199
+ def recipient(klass)
200
+ raise "Routing class already defined: #{self.recipient_class}" unless self.recipient_class.blank?
201
+
202
+ # inject sugar methods
203
+ klass.class_eval <<-EOS, __FILE__, __LINE__
204
+ # fetch latest timeline entries
205
+ def #{self.kind}(limit, options = { })
206
+ Activr.timeline(#{self.name}, self.id).find(limit, options)
207
+ end
208
+
209
+ # get total number of timeline entries
210
+ def #{self.kind}_count
211
+ Activr.timeline(#{self.name}, self.id).count
212
+ end
213
+ EOS
214
+
215
+ self.recipient_class = klass
216
+ end
217
+
218
+ # Set maximum length
219
+ #
220
+ # @param value [Integer] Maximum timeline length
221
+ def max_length(value)
222
+ self.trim_max_length = value
223
+ end
224
+
225
+ # Creates a predefined routing
226
+ #
227
+ # You can either specify a `Proc` (with the `:to` setting) to execute or a `block` to yield everytime
228
+ # an activity is routed to that timeline. That `Proc` or that `block` must return an array of recipients
229
+ # or recipients ids.
230
+ #
231
+ # @param routing_name [Symbol,String] Routing name
232
+ # @param settings [Hash] Settings
233
+ # @option settings [Proc] :to Code to resolve route
234
+ # @yield [Activity] Gives the activity to route to the block
235
+ def routing(routing_name, settings = { }, &block)
236
+ routing_name = routing_name.to_s
237
+ raise "Routing already defined: #{routing_name}" unless self.routings[routing_name].blank?
238
+
239
+ if !block && (!settings[:to] || !settings[:to].is_a?(Proc))
240
+ raise "No routing logic provided for #{routing_name}: #{settings.inspect}"
241
+ end
242
+
243
+ if block
244
+ raise "It is forbidden to provide a block AND a :to setting" if settings[:to]
245
+ settings = settings.merge(:to => block)
246
+ end
247
+
248
+ # NOTE: always use a setter on a class_attribute (cf. http://apidock.com/rails/Class/class_attribute)
249
+ self.routings = self.routings.merge(routing_name => settings)
250
+
251
+ # create method
252
+ class_eval <<-EOS, __FILE__, __LINE__
253
+ # eg: actor_follower(activity)
254
+ def self.#{routing_name}(activity)
255
+ self.routings['#{routing_name}'][:to].call(activity)
256
+ end
257
+ EOS
258
+ end
259
+
260
+ # Define a route for an activity
261
+ #
262
+ # @param activity_class [Class] Activity to route
263
+ # @param settings (see Timeline::Route#initialize)
264
+ # @option settings (see Timeline::Route#initialize)
265
+ def route(activity_class, settings = { })
266
+ new_route = Activr::Timeline::Route.new(self, activity_class, settings)
267
+ raise "Route already defined: #{new_route.inspect}" if self.have_route?(new_route)
268
+
269
+ # NOTE: always use a setter on a class_attribute (cf. http://apidock.com/rails/Class/class_attribute)
270
+ self.routes += [ new_route ]
271
+ end
272
+
273
+ end # class << self
274
+
275
+
276
+ extend Forwardable
277
+
278
+ # Forward methods to class
279
+ def_delegators "self.class",
280
+ :kind,
281
+ :route_for_kind, :route_for_routing_and_activity, :routes_for_activity, :have_route?
282
+
283
+
284
+ # @param rcpt Recipient instance, or recipient id
285
+ def initialize(rcpt)
286
+ if self.recipient_class.nil?
287
+ raise "Missing recipient_class attribute for timeline: #{self}"
288
+ end
289
+
290
+ if rcpt.is_a?(self.recipient_class)
291
+ @recipient = rcpt
292
+ @recipient_id = rcpt.id
293
+ else
294
+ @recipient = nil
295
+ @recipient_id = rcpt
296
+ end
297
+
298
+ if (@recipient.blank? && @recipient_id.blank?)
299
+ raise "No recipient provided"
300
+ end
301
+ end
302
+
303
+ # Get recipient instance
304
+ #
305
+ # @return [Object] Recipient instance
306
+ def recipient
307
+ @recipient ||= self.recipient_class.find(@recipient_id)
308
+ end
309
+
310
+ # Get recipient id
311
+ #
312
+ # @return [Object] Recipient id
313
+ def recipient_id
314
+ @recipient_id ||= @recipient.id
315
+ end
316
+
317
+ # Handle activity
318
+ #
319
+ # @param activity [Activity] Activity to handle
320
+ # @param route [Timeline::Route] The route that caused that activity handling
321
+ # @return [Timeline::Entry] Created timeline entry
322
+ def handle_activity(activity, route)
323
+ # create timeline entry
324
+ klass = Activr.registry.class_for_timeline_entry(self.kind, route.kind)
325
+ timeline_entry = klass.new(self, route.routing_kind, activity)
326
+
327
+ # store with callbacks
328
+ if self.should_store_timeline_entry?(timeline_entry)
329
+ self.will_store_timeline_entry(timeline_entry)
330
+
331
+ # store
332
+ timeline_entry.store!
333
+
334
+ self.did_store_timeline_entry(timeline_entry)
335
+
336
+ # trim timeline
337
+ self.trim!
338
+ end
339
+
340
+ timeline_entry._id.blank? ? nil :timeline_entry
341
+ end
342
+
343
+ # Find timeline entries by descending timestamp
344
+ #
345
+ # @param limit (see Storage#find_timeline)
346
+ # @param options (see Storage#find_timeline)
347
+ # @option options (see Storage#find_timeline)
348
+ # @return (see Storage#find_timeline)
349
+ def find(limit, options = { })
350
+ Activr.storage.find_timeline(self, limit, options)
351
+ end
352
+
353
+ # Get total number of timeline entries
354
+ #
355
+ # @param options (see Storage#count_timeline)
356
+ # @option options (see Storage#count_timeline)
357
+ # @return (see Storage#count_timeline)
358
+ def count(options = { })
359
+ Activr.storage.count_timeline(self, options)
360
+ end
361
+
362
+ # Dump humanization of last timeline entries
363
+ #
364
+ # @param options [Hash] Options hash
365
+ # @option options (see Activr::Timeline::Entry#humanize)
366
+ # @option options [Integer] :nb Number of timeline entries to dump (default: 100)
367
+ # @return [Array<String>] Array of humanized sentences
368
+ def dump(options = { })
369
+ options = options.dup
370
+
371
+ limit = options.delete(:nb) || 100
372
+
373
+ self.find(limit).map{ |tl_entry| tl_entry.humanize(options) }
374
+ end
375
+
376
+ # Delete timeline entries
377
+ #
378
+ # @param options (see Storage#delete_timeline)
379
+ # @option options (see Storage#delete_timeline)
380
+ def delete(options = { })
381
+ Activr.storage.delete_timeline(self, options)
382
+ end
383
+
384
+ # Remove old timeline entries
385
+ def trim!
386
+ # check if trimming is needed
387
+ if (self.trim_max_length > 0) && (self.count > self.trim_max_length)
388
+ last_tle = self.find(1, :skip => self.trim_max_length - 1).first
389
+ if last_tle
390
+ self.delete(:before => last_tle.activity.at)
391
+ end
392
+ end
393
+ end
394
+
395
+
396
+ #
397
+ # Callbacks
398
+ #
399
+
400
+ # Callback: just before trying to handle routed activity
401
+ #
402
+ # @note MAY be overriden by child class
403
+ #
404
+ # @param activity [Activity] Activity to handle
405
+ # @param route [Timeline::Route] Route that caused that handling
406
+ # @return [true,false] Returns `false` to skip activity
407
+ def should_handle_activity?(activity, route)
408
+ true
409
+ end
410
+
411
+ # Callback: check if given timeline entry should be stored
412
+ #
413
+ # @note MAY be overriden by child class
414
+ #
415
+ # @param timeline_entry [Timeline::Entry] Timeline entry that should be stored
416
+ # @return [true,false] Returns `false` to cancel storing
417
+ def should_store_timeline_entry?(timeline_entry)
418
+ true
419
+ end
420
+
421
+ # Callback: just before storing timeline entry into timeline
422
+ #
423
+ # @note MAY be overriden by child class
424
+ #
425
+ # @param timeline_entry [Timeline::Entry] Timeline entry that will be stored
426
+ def will_store_timeline_entry(timeline_entry)
427
+ # NOOP
428
+ end
429
+
430
+ # Callback: just after timeline entry was stored
431
+ #
432
+ # @note MAY be overriden by child class
433
+ #
434
+ # @param timeline_entry [Timeline::Entry] Timeline entry that has been stored
435
+ def did_store_timeline_entry(timeline_entry)
436
+ # NOOP
437
+ end
438
+
439
+ end # class Timeline
440
+
441
+ end # module Activr
@@ -0,0 +1,165 @@
1
+ #
2
+ # A timeline entry correspond to an activity routed to a timeline
3
+ #
4
+ # When instanciated, it contains:
5
+ # - The `timeline` it belongs to
6
+ # - A copy of the routed `activity`
7
+ # - The `routing_kind` that indicates how that `activity` has been routed
8
+ # - User-defined `meta` data
9
+ #
10
+ class Activr::Timeline::Entry
11
+
12
+ extend ActiveModel::Callbacks
13
+
14
+ # callbacks when timeline entry is stored
15
+ define_model_callbacks :store
16
+
17
+
18
+ class << self
19
+
20
+ # Instanciate a timeline entry from a hash
21
+ #
22
+ # @param hash [Hash] Timeline entry hash
23
+ # @param timeline [Timeline] Timeline instance
24
+ # @return [Timeline::Entry] Timeline entry instance
25
+ def from_hash(hash, timeline)
26
+ activity_hash = hash['activity'] || hash[:activity]
27
+ raise "No activity found in timeline entry hash: #{hash.inspect}" if activity_hash.blank?
28
+
29
+ activity_kind = activity_hash['kind'] || activity_hash[:kind]
30
+ raise "No activity kind in timeline entry activity: #{activity_hash.inspect}" if activity_kind.blank?
31
+
32
+ routing_kind = hash['routing'] || hash[:routing]
33
+ raise "No routing_kind found in timeline entry hash: #{hash.inspect}" if routing_kind.blank?
34
+
35
+ activity = Activr::Activity.from_hash(activity_hash)
36
+ route_kind = Activr::Timeline::Route.kind_for_routing_and_activity(routing_kind, activity_kind)
37
+
38
+ klass = Activr.registry.class_for_timeline_entry(timeline.kind, route_kind)
39
+ result = klass.new(timeline, routing_kind, activity, hash['meta'] || hash[:meta])
40
+ result._id = hash['_id'] || hash[:_id]
41
+
42
+ result
43
+ end
44
+
45
+ end # class << self
46
+
47
+ # @return [Object] timeline entry id
48
+ attr_accessor :_id
49
+
50
+ # @return [Timeline] timeline that owns that timeline entry
51
+ attr_reader :timeline
52
+
53
+ # @return [String] routing kind
54
+ attr_reader :routing_kind
55
+
56
+ # @return [Activity] embedded activity
57
+ attr_reader :activity
58
+
59
+ # @return [Hash] meta data
60
+ attr_reader :meta
61
+
62
+
63
+ # @param timeline [Timeline] Timeline instance
64
+ # @param routing_kind [String] Routing kind
65
+ # @param activity [Activity] Activity
66
+ # @param meta [Hash] Meta data
67
+ def initialize(timeline, routing_kind, activity, meta = { })
68
+ @timeline = timeline
69
+ @routing_kind = routing_kind
70
+ @activity = activity
71
+ @meta = meta && meta.symbolize_keys
72
+ end
73
+
74
+ # Get a meta
75
+ #
76
+ # @example
77
+ # timeline_entry[:foo]
78
+ # # => 'bar'
79
+ #
80
+ # @param key [Symbol] Meta name
81
+ # @return [Object] Meta value
82
+ def [](key)
83
+ @meta[key.to_sym]
84
+ end
85
+
86
+ # Set a meta
87
+ #
88
+ # @example
89
+ # timeline_entry[:foo] = 'bar'
90
+ #
91
+ # @param key [Symbol] Meta name
92
+ # @param value [Object] Meta value
93
+ def []=(key, value)
94
+ @meta[key.to_sym] = value
95
+ end
96
+
97
+ # Serialize timeline entry to a hash
98
+ #
99
+ # @note All keys are stringified (ie. there is no Symbol)
100
+ #
101
+ # @return [Hash] Timeline entry hash
102
+ def to_hash
103
+ # fields
104
+ result = {
105
+ 'rcpt' => @timeline.recipient_id,
106
+ 'routing' => @routing_kind,
107
+ 'activity' => @activity.to_hash,
108
+ }
109
+
110
+ result['meta'] = @meta.stringify_keys unless @meta.blank?
111
+
112
+ result
113
+ end
114
+
115
+ # Get the corresponding timeline route
116
+ #
117
+ # @return [Timeline::Route] The route instance
118
+ def timeline_route
119
+ @timeline_route ||= begin
120
+ result = @timeline.route_for_kind(Activr::Timeline::Route.kind_for_routing_and_activity(@routing_kind, @activity.kind))
121
+ raise "Failed to find a route for #{@routing_kind} / #{@activity.kind}: #{self.inspect}" if result.nil?
122
+ result
123
+ end
124
+ end
125
+
126
+ # (see Activity#humanization_bindings)
127
+ def humanization_bindings(options = { })
128
+ @activity.humanization_bindings(options)
129
+ end
130
+
131
+ # Humanize that timeline entry
132
+ #
133
+ # @note MAY be overriden by child class for specialized humanization
134
+ #
135
+ # @param options (see Activity#humanize)
136
+ # @option options (see Activity#humanize)
137
+ # @return [String] Humanized timeline entry
138
+ def humanize(options = { })
139
+ if !self.timeline_route.settings[:humanize].blank?
140
+ # specialized humanization
141
+ Activr.sentence(self.timeline_route.settings[:humanize], self.humanization_bindings(options))
142
+ else
143
+ # default humanization
144
+ @activity.humanize(options)
145
+ end
146
+ end
147
+
148
+ # Is it already stored
149
+ #
150
+ # @return [true, false]
151
+ def stored?
152
+ !@_id.nil?
153
+ end
154
+
155
+ # Store in database
156
+ #
157
+ # @note SIDE EFFECT: The `_id` field is set
158
+ def store!
159
+ run_callbacks(:store) do
160
+ # store
161
+ @_id = Activr.storage.insert_timeline_entry(self)
162
+ end
163
+ end
164
+
165
+ end # class Activr::Timeline::Entry