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