17

I am trying to fuse to objects into a single stream.

I created a new model and controller called Newsfeed. I have it so that newsfeed has_many :messages and has_many :images and images and messages belong_to :newsfeed. I am setting up the controller right now and I have:

def index
    @messages = Messages.all
    @images = Images.all
    @feed = ?
end

How do I fuse them so it can pull from both and put them in chronological order when each requires separate formating?

Any help would be greatly appreciated, thanks.

Cody Brown
  • 203
  • 3
  • 6

3 Answers3

18

You could do something as :

controller:

def index
    messages = Messages.all
    images = Images.all
    @feed = (messages + images).order_by(&:created_at)
end

view index.html.erb :

<%= render @feed %>

view _feed.html.erb :

<% if feed.is_a? Message %>
  <%= render "message", :locals => {:message => feed}%>
<% else %>
  <%= render "image", :locals => {:image => feed}%>
<% end %>

And add 2 partials _message.html.erb and _image.html.erb

Mike
  • 5,165
  • 6
  • 35
  • 50
  • I also write an article on how to do it controller/model side in a more *elegant* way http://mickeyben.com/2010/05/23/creating-an-activity-feed-with-rails%2C-ar-and-observers.html – Mike Mar 29 '11 at 10:35
7

I'm sure this can be streamlined from it's ghetto-polymorphism, but it works for me, and I had an identical struggle with the "how do I combine all these model results?!"

I use a separate model, like you, and an observer:

class NewsfeedObserver < ActiveRecord::Observer

  observe Message, Image

  def after_save(object)
    @item = Newsfeed.create(
              :item_id => object.id, 
              :item_type => object.class.to_s
    )
  end
end

Then in the Newsfeed model you can pull and collate as you like:

def self.get_items
  item_list = []
  items = find(:all, :order => "created_at DESC", :limit => 30)
  items.collect{ |i| i.item_type.constantize.find(i.item_id) }
end

Then just pull them and display in your view however you like.

#Controller
def index
  @feed = Newsfeed.get_items
end

Moving forward I will probably start to pack more into the stream-tracking model in order to save hits on the other tables, but hopefully you get the idea.

Eric
  • 2,539
  • 18
  • 23
1

This feels like a bit of a loaded question, and though I don't have the time to dig into it in full, here are some initial thoughts for you:

  1. you can combine the two types of items you want to display into a single array and then sort this array based on a field which ought to be common between the two (created_at comes to mind) to get them into chronological order.

  2. In the view, you can use this single collection to iterate over and then use helpers and unique partials to render the items in the correct format. This could be done by checking the objects type as you are trying to display it in the view.

Something along the lines of:

if item.is_a? Message
  render :partial => 'message', :locals => {:message => item }
elsif item.is_a? Image
  render :partial => 'image', :locals => {:image => item }
end

hopefully this helps some!

EDIT: to combine the arrays, you can simply append them together using the '+' operator, since arrays in Ruby do not need to contain a single object type.

your Messages.all and Images.all calls give you arrays of objects, so just try

@messages + @images 

and you should have your combined array.

EDIT #2: If I were writing this myself, I would probably handle the views somewhat like this:

at the top level, simply pass your feeds collection to a partial which is designed to render a single item:

render :partial => "item", :collection => @feed

this will render the '_item.html.erb' partial for each item in @feed. Also, it will give the partial a local variable named 'item' each time, which is the item in @feed for that particular iteration.

now, I would probably have the '_item' partial be very simple, like this:

<%= display_item(item) %>

where display_item is a helper method outside the view and is implemented with the "is_a?" type logic from above to choose the correct display format for the item.

the reason I would break it up like this is it takes advantage of some nice rails features (pass a collection to a partial instead of looping over the collection yourself) and then pulling the logic out into helpers (or even the models if it makes sense to) is useful because it simplifies your views. keeping logic out of the view whenever possible is generally a good practice as such logic can be hard to test and your views will become very hard to read.

Pete
  • 17,885
  • 4
  • 32
  • 30
  • Thanks for the reply, how would I combine them into a single array int the controller? Smart idea on the partials, I forgot about that. Why do you think it's a loaded question? I'm a rails noob. – Cody Brown Feb 11 '10 at 04:25
  • 1
    At first glance it seemed like a lot of detail could be needed to get a solid answer to the question, but hopefully these ideas are useful. I updated the answer with how to append the arrays together. – Pete Feb 11 '10 at 04:45
  • Hey Pete, I'm sorry I'm asking so many questions but I have all the code in place except the helpers. How would I define item.is_a? Message? Thanks again. – Cody Brown Feb 11 '10 at 05:41
  • also do I set it up in the view as: <% @feed.each do |feed| %> <% if item.is_a? Message %> <%= render :partial => "message", :locals => { :messages => @messages }%> <% elseif item.is_a? Image %> <%= render :partial => "image", :locals => { :images => @images }%> <% end %> <% end %> – Cody Brown Feb 11 '10 at 05:45
  • is_a? is a built in method in Ruby, so you do not need to define it yourself. – Pete Feb 11 '10 at 12:39
  • updated answer for my take on the display in a bit more detail – Pete Feb 11 '10 at 12:56
  • As for looping through the feed in the view, you can replace all of that with `<%= render feed.class %>` instead of checking for every possible `is_a?` class. – Eric Dec 05 '10 at 19:10