Class: Activr::Timeline

Inherits:
Object
  • Object
show all
Extended by:
Forwardable
Defined in:
lib/activr/timeline.rb

Overview

With a timeline you can create complex activity feeds.

When creating a Timeline class you specify:

- what model in your application owns that timeline: the `recipient`
- what activities will be displayed in that timeline: the `routes`

Routes can be resolved thanks to:

- a predefined routing declared with routing method, then specified in the :using route setting
- an activity path specified in the :to route setting
- a call on timeline class method specified in the :using route setting

When an activity is routed to a timeline, a Timeline Entry is stored in database and that Timeline Entry contains a copy of the original activity: so Activr uses a “Fanout on write” mecanism to dispatch activities to timelines.

Several callbacks are invoked on timeline instance during the activity handling workflow:

- .should_route_activity?       - Returns `false` to skip activity routing
- #should_handle_activity?      - Returns `false` to skip routed activity
- #should_store_timeline_entry? - Returns `false` to cancel timeline entry storing
- #will_store_timeline_entry    - This is your last chance to modify timeline entry before it is stored
- #did_store_timeline_entry     - Called just after timeline entry was stored

Examples:

For example, this is a user newsfeed timeline


class UserNewsFeedTimeline < Activr::Timeline
  # that timeline is for users
  recipient User

  # this is a predefined routing, to fetch all followers of an activity actor
  routing :actor_follower, :to => Proc.new{ |activity| activity.actor.followers }

  # define a routing with a class method, to fetch all followers of an activity album
  def self.album_follower(activity)
    activity.album.followers
  end

  # predefined routing: users will see in their news feed when a friend they follow likes a picture
  route LikePictureActivity, :using => :actor_follower

  # activity path: users will see in their news feed when someone adds a picture in one of their albums
  route AddPictureActivity, :to => 'album.owner', :humanize => "{{{actor}}} added a picture to your album {{{album}}}"

  # method call: users will see in their news feed when someone adds a picture in an album they follow
  route AddPictureActivity, :using => :album_follower

end

Defined Under Namespace

Classes: Entry, Route

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(rcpt) ⇒ Timeline



285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
# File 'lib/activr/timeline.rb', line 285

def initialize(rcpt)
  if self.recipient_class.nil?
    raise "Missing recipient_class attribute for timeline: #{self}"
  end

  if rcpt.is_a?(self.recipient_class)
    @recipient    = rcpt
    @recipient_id = rcpt.id
  else
    @recipient    = nil
    @recipient_id = rcpt
  end

  if (@recipient.blank? && @recipient_id.blank?)
    raise "No recipient provided"
  end
end

Class Method Details

.have_route?(route_to_check) ⇒ true, false

Check if given route was already defined



133
134
135
# File 'lib/activr/timeline.rb', line 133

def have_route?(route_to_check)
  (route_to_check.timeline_class == self) && !self.route_for_kind(route_to_check.kind).blank?
end

.kindString

Note:

Kind is inferred from Class name, unless ‘#set_kind` method is used to force a custom value

Get timeline class kind

Examples:

UserNewsFeedTimeline.kind
# => 'user_news_feed'


87
88
89
# File 'lib/activr/timeline.rb', line 87

def kind
  @kind ||= @forced_kind || Activr::Utils.kind_for_class(self, 'timeline')
end

.max_length(value) ⇒ Object

Set maximum length



221
222
223
# File 'lib/activr/timeline.rb', line 221

def max_length(value)
  self.trim_max_length = value
end

.recipient(klass) ⇒ Object

Set recipient class

Examples:

Several instance methods are injected in given ‘klass`, for example with timeline:


class UserNewsFeedTimeline < Activr::Timeline
  recipient User

  # ...
end

Those methods are created:


class User
  # fetch latest timeline entries
  def user_news(limit, options = { })
    # ...
  end

  # get total number of timeline entries
  def user_news_count
    # ...
  end
end


199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
# File 'lib/activr/timeline.rb', line 199

def recipient(klass)
  raise "Routing class already defined: #{self.recipient_class}" unless self.recipient_class.blank?

  # inject sugar methods
  klass.class_eval <<-EOS, __FILE__, __LINE__
    # fetch latest timeline entries
    def #{self.kind}(limit, options = { })
      Activr.timeline(#{self.name}, self.id).find(limit, options)
    end

    # get total number of timeline entries
    def #{self.kind}_count
      Activr.timeline(#{self.name}, self.id).count
    end
  EOS

  self.recipient_class = klass
end

.recipient_id(recipient) ⇒ Object

Get recipient id for given recipient



159
160
161
162
163
164
165
166
167
# File 'lib/activr/timeline.rb', line 159

def recipient_id(recipient)
  if self.recipient_class && recipient.is_a?(self.recipient_class)
    recipient.id
  elsif Activr.storage.valid_id?(recipient)
    recipient
  else
    raise "Invalid recipient #{recipient.inspect} for timeline #{self}"
  end
end

.route(activity_class, settings = { }) ⇒ Object

Define a route for an activity

Options Hash (settings):

  • :using (Symbol)

    Predefined routing kind

  • :to (Symbol)

    Routing path

  • :kind (Symbol)

    Manually specified routing kind



265
266
267
268
269
270
271
# File 'lib/activr/timeline.rb', line 265

def route(activity_class, settings = { })
  new_route = Activr::Timeline::Route.new(self, activity_class, settings)
  raise "Route already defined: #{new_route.inspect}" if self.have_route?(new_route)

  # NOTE: always use a setter on a class_attribute (cf. http://apidock.com/rails/Class/class_attribute)
  self.routes += [ new_route ]
end

.route_for_kind(route_kind) ⇒ Timeline::Route

Get route defined with given kind



104
105
106
107
108
# File 'lib/activr/timeline.rb', line 104

def route_for_kind(route_kind)
  self.routes.find do |defined_route|
    (defined_route.kind == route_kind)
  end
end

.route_for_routing_and_activity(routing_kind, activity_class) ⇒ Timeline::Route

Get route defined with given kind



115
116
117
# File 'lib/activr/timeline.rb', line 115

def route_for_routing_and_activity(routing_kind, activity_class)
  self.route_for_kind(Activr::Timeline::Route.kind_for_routing_and_activity(routing_kind, activity_class.kind))
end

.routes_for_activity(activity_class) ⇒ Array<Timeline::Route>

Get all routes defined for given activity



123
124
125
126
127
# File 'lib/activr/timeline.rb', line 123

def routes_for_activity(activity_class)
  self.routes.find_all do |defined_route|
    (defined_route.activity_class == activity_class)
  end
end

.routing(routing_name, settings = { }) {|Activity| ... } ⇒ Object

Creates a predefined routing

You can either specify a ‘Proc` (with the `:to` setting) to execute or a `block` to yield everytime an activity is routed to that timeline. That `Proc` or that `block` must return an array of recipients or recipients ids.

Options Hash (settings):

  • :to (Proc)

    Code to resolve route

Yields:

  • (Activity)

    Gives the activity to route to the block



235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/activr/timeline.rb', line 235

def routing(routing_name, settings = { }, &block)
  routing_name = routing_name.to_s
  raise "Routing already defined: #{routing_name}" unless self.routings[routing_name].blank?

  if !block && (!settings[:to] || !settings[:to].is_a?(Proc))
    raise "No routing logic provided for #{routing_name}: #{settings.inspect}"
  end

  if block
    raise "It is forbidden to provide a block AND a :to setting" if settings[:to]
    settings = settings.merge(:to => block)
  end

  # NOTE: always use a setter on a class_attribute (cf. http://apidock.com/rails/Class/class_attribute)
  self.routings = self.routings.merge(routing_name => settings)

  # create method
  class_eval <<-EOS, __FILE__, __LINE__
    # eg: actor_follower(activity)
    def self.#{routing_name}(activity)
      self.routings['#{routing_name}'][:to].call(activity)
    end
  EOS
end

.set_kind(forced_kind) ⇒ Object

Note:

Default kind is inferred from class name

Set timeline kind



96
97
98
# File 'lib/activr/timeline.rb', line 96

def set_kind(forced_kind)
  @forced_kind = forced_kind.to_s
end

.should_route_activity?(activity) ⇒ true, false

Note:

MAY be overriden by child class

Callback: just before trying to route given activity



143
144
145
# File 'lib/activr/timeline.rb', line 143

def should_route_activity?(activity)
  true
end

.valid_recipient?(recipient) ⇒ true, false

Is it a valid recipient



151
152
153
# File 'lib/activr/timeline.rb', line 151

def valid_recipient?(recipient)
  (self.recipient_class && recipient.is_a?(self.recipient_class)) || Activr.storage.valid_id?(recipient)
end

Instance Method Details

#count(options = { }) ⇒ Integer

Get total number of timeline entries

Options Hash (options):



358
359
360
# File 'lib/activr/timeline.rb', line 358

def count(options = { })
  Activr.storage.count_timeline(self, options)
end

#delete(options = { }) ⇒ Object

Delete timeline entries

Options Hash (options):

  • :before (Time)

    Delete only timeline entries which timestamp is before that datetime (excluding)

  • :entity (Hash{Sym=>String})

    Delete only timeline entries with these entities values



380
381
382
# File 'lib/activr/timeline.rb', line 380

def delete(options = { })
  Activr.storage.delete_timeline(self, options)
end

#did_store_timeline_entry(timeline_entry) ⇒ Object

Note:

MAY be overriden by child class

Callback: just after timeline entry was stored



435
436
437
# File 'lib/activr/timeline.rb', line 435

def did_store_timeline_entry(timeline_entry)
  # NOOP
end

#dump(options = { }) ⇒ Array<String>

Dump humanization of last timeline entries

Options Hash (options):

  • :nb (Integer)

    Number of timeline entries to dump (default: 100)

  • :html (true, false)

    Output HTML (default: ‘false`)



368
369
370
371
372
373
374
# File 'lib/activr/timeline.rb', line 368

def dump(options = { })
  options = options.dup

  limit = options.delete(:nb) || 100

  self.find(limit).map{ |tl_entry| tl_entry.humanize(options) }
end

#find(limit, options = { }) ⇒ Array<Timeline::Entry>

Find timeline entries by descending timestamp

Options Hash (options):

  • :skip (Integer)

    Number of entries to skip (default: 0)

  • :only (Array<Timeline::Route>)

    An array of routes to fetch



349
350
351
# File 'lib/activr/timeline.rb', line 349

def find(limit, options = { })
  Activr.storage.find_timeline(self, limit, options)
end

#handle_activity(activity, route) ⇒ Timeline::Entry

Handle activity



322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
# File 'lib/activr/timeline.rb', line 322

def handle_activity(activity, route)
  # create timeline entry
  klass = Activr.registry.class_for_timeline_entry(self.kind, route.kind)
  timeline_entry = klass.new(self, route.routing_kind, activity)

  # store with callbacks
  if self.should_store_timeline_entry?(timeline_entry)
    self.will_store_timeline_entry(timeline_entry)

    # store
    timeline_entry.store!

    self.did_store_timeline_entry(timeline_entry)

    # trim timeline
    self.trim!
  end

  timeline_entry._id.blank? ? nil :timeline_entry
end

#recipientObject

Get recipient instance



306
307
308
# File 'lib/activr/timeline.rb', line 306

def recipient
  @recipient ||= self.recipient_class.find(@recipient_id)
end

#recipient_idObject

Get recipient id



313
314
315
# File 'lib/activr/timeline.rb', line 313

def recipient_id
  @recipient_id ||= @recipient.id
end

#should_handle_activity?(activity, route) ⇒ true, false

Note:

MAY be overriden by child class

Callback: just before trying to handle routed activity



407
408
409
# File 'lib/activr/timeline.rb', line 407

def should_handle_activity?(activity, route)
  true
end

#should_store_timeline_entry?(timeline_entry) ⇒ true, false

Note:

MAY be overriden by child class

Callback: check if given timeline entry should be stored



417
418
419
# File 'lib/activr/timeline.rb', line 417

def should_store_timeline_entry?(timeline_entry)
  true
end

#trim!Object

Remove old timeline entries



385
386
387
388
389
390
391
392
393
# File 'lib/activr/timeline.rb', line 385

def trim!
  # check if trimming is needed
  if (self.trim_max_length > 0) && (self.count > self.trim_max_length)
    last_tle = self.find(1, :skip => self.trim_max_length - 1).first
    if last_tle
      self.delete(:before => last_tle.activity.at)
    end
  end
end

#will_store_timeline_entry(timeline_entry) ⇒ Object

Note:

MAY be overriden by child class

Callback: just before storing timeline entry into timeline



426
427
428
# File 'lib/activr/timeline.rb', line 426

def will_store_timeline_entry(timeline_entry)
  # NOOP
end