activr 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.md +764 -0
- data/Rakefile +50 -0
- data/lib/activr.rb +206 -0
- data/lib/activr/activity.rb +446 -0
- data/lib/activr/async.rb +92 -0
- data/lib/activr/async/resque.rb +58 -0
- data/lib/activr/configuration.rb +40 -0
- data/lib/activr/dispatcher.rb +70 -0
- data/lib/activr/entity.rb +103 -0
- data/lib/activr/entity/model_mixin.rb +117 -0
- data/lib/activr/rails_ctx.rb +37 -0
- data/lib/activr/railtie.rb +73 -0
- data/lib/activr/railties/activr.rake +14 -0
- data/lib/activr/registry.rb +268 -0
- data/lib/activr/storage.rb +404 -0
- data/lib/activr/storage/mongo_driver.rb +645 -0
- data/lib/activr/timeline.rb +441 -0
- data/lib/activr/timeline/entry.rb +165 -0
- data/lib/activr/timeline/route.rb +161 -0
- data/lib/activr/utils.rb +74 -0
- data/lib/activr/version.rb +6 -0
- data/lib/generators/activr/activity_generator.rb +91 -0
- data/lib/generators/activr/templates/activity.rb +28 -0
- data/lib/generators/activr/templates/timeline.rb +42 -0
- data/lib/generators/activr/timeline_generator.rb +24 -0
- metadata +197 -0
@@ -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
|