activr 1.0.0

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.
@@ -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