Question

I am building a model which keeps track of posts that a given user has read (in Rails 3). The table that tracks this is 'readings.' a reading belongs_to both :user and :post, both of which has_many :readings. When a user sees a post, it is marked as read, and a 'reading' is created with the user_id and post_id. This seems to work fine, but now I am trying to write a scope that returns only unread posts for a given user. Finding read posts works fine:

scope :read, lambda { |user_id = nil |       
    user_id.nil? ? {} : joins(:readings).where('readings.user_id = :user_id', :user_id => user_id)
}

However my attempts to return the complement of this set (unread posts) are failing. How can this be done? Is there a general way to write a scope(method) that just returns the complement of another(here, :read)? I would also be happy to write this as a class method that works the same as a scope.

Bear in mind that readings are only created when a post is read, so a new post would have no readings. This seemed more economical than creating entries for every user for every new post.

EDIT: OK, I am getting closer - the below scope almost works, but does not require that all readings for a post are not equal to user_id. Thus if user #1 has read a post, and so has user #2, that other reading is queried and found to be not equal. So the post is wrongly found to be unread.

scope :unread, lambda { |user_id = nil |       
    user_id.nil? ? {} : includes(:readings).where('(readings.user_id IS NULL OR readings.user_id != :user_id)', :user_id => user_id)
}
Was it helpful?

Solution

Well, it looks like the below works. Still testing though. If anyone finds a hole in here happy to re-open.

scope :unread_by, lambda { |user_id = nil, hide_unread = nil|
      { :joins => "LEFT JOIN readings ON  readings.post_id    = post.id
          AND readings.user_id        = #{user_id}",
          :conditions => 'readings.id IS NULL' }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top