views:

19

answers:

1

I have a stream of activities (Activity) belonging to users and with a created_at. I'd like to present a stream of the 10 latest activities, with the caveat that I want to group activities together. I know I can use group_by, but I don't want to group everything together by user, only those activities that appear > 2 times consecutively.

  • Tom said something
  • Joe did something
  • Joe kicked something
  • Joe punched something
  • Joe said something
  • Bill did something
  • Bill sent something

If it was grouped, it would be like so:

  • Tom said something
  • Joe did something
  • Joe kicked something [More from Joe...]
  • Bill did something
  • Bill sent something

Where the More to link would open up a hidden div.

A: 

I don't think there's any special way to do this. You just have to do it.

Let's say you could efficiently grab all activities and their associated users. You likely can't do this efficiently, for something as frequent as an activity on a website, but hypothetically let's say you could, for simplicity's sake.

What you want is to iterate your activities, and collect lines you want to display. The easy road, in my humble opinion, would simply be a finder-like method on your Activity model, that returns arrays of consecutive actions by a user.

Leave your thresholds to the view. (No more than 2 consecutive actions, no more than 10 lines total.)

If Ruby has some fancy way of collecting consecutive elements from an array, then I'm probably going to look stupid, but here's some (untested) code:

class Activity < ActiveRecord::Base
  belongs_to :user

  def self.each_consecutive_group(options={})
    default_options = { :order => 'created_at DESC' }

    current_group = nil
    last_user_id = false
    self.all(default_options.merge(options)).each do |activity|
      # Look for consecutive activities from the same user.
      # (This is never true on the first iteration.)
      if last_user_id == activity.user_id
        current_group << activity
      else
        # The nil case happens during first iteration.
        yield current_group unless current_group.nil?

        # Initialize state for the next group of activities.
        current_group = [activity]
        last_user_id = activity.user_id
      end
    end
    # Do we still have something to yield?
    yield current_group if current_group
  end

  # Just return the above as an array, rather than iterating.
  def self.consecutive_groups(options={})
    groups = []
    self.each_consecutive_group(options) { |group| groups << group }
    groups
  end
end

# Usage:
Activity.each_grouped(:include => :user, :limit => 30) do |group|
  # [...]
end

# Or if you want to pass something from the controller to the view:
@activity_groups = Activity.grouped(:include => :user, :limit => 30)

To then mitigate the performance problem, I'd simply take a safe margin. If 10 lines is what you want, you could pick a scope of 20 activities to search through, and there would be room for 10 records to be hidden. Any more, and you might see less than 10 lines. Selecting 20 records is hardly a lot though, and you may choose to expand that number.

If you're die-hard on always showing 10 lines, then you probably have to move the threshold code into the above any ways. You could then use find_in_batches, rather than :limit, and keep looping until your threshold is exceeded.

Shtééf