I am putting together a messaging system for a rails app I am working on.
I am building it in a similar fashion to facebook's system, so messages are grouped into threads, etc.
My related models are:
MsgThread - main container of a thread
Message - each message/reply in thread
Recipience - ties to user to define which users should subscribe to this thread
Read - determines whether or not a user has read a specific message
My relationships look like
class User < ActiveRecord::Base
#stuff...
has_many :msg_threads, :foreign_key => 'originator_id' #threads the user has started
has_many :recipiences
has_many :subscribed_threads, :through => :recipiences, :source => :msg_thread #threads the user is subscribed to
end
class MsgThread < ActiveRecord::Base
has_many :messages
has_many :recipiences
belongs_to :originator, :class_name => "User", :foreign_key => "originator_id"
end
class Recipience < ActiveRecord::Base
belongs_to :user
belongs_to :msg_thread
end
class Message < ActiveRecord::Base
belongs_to :msg_thread
belongs_to :author, :class_name => "User", :foreign_key => "author_id"
end
class Read < ActiveRecord::Base
belongs_to :user
belongs_to :message
end
I'd like to create a new selector in the user sort of like:
has_many :updated_threads, :through => :recipiencies, :source => :msg_thread, :conditions => {THREAD CONTAINS MESSAGES WHICH ARE UNREAD (have no 'read' models tying a user to a message)}
I was thinking of either writing a long condition with multiple joins, or possibly writing giving the model an updated_threads method to return this, but I'd like to see if there is an easier way first. Am I able to pass some kind of nested hash into the conditions instead of a string?
Any ideas? Also, if there is something fundamentally wrong with my structure for this functionality let me know! Thanks!!
UPDATE:
While I would still appreciate input on better possibilities if they exist, this is what I have gotten working now:
class User < ActiveRecord::Base
# stuff...
def updated_threads
MsgThread.find_by_sql("
SELECT msg_threads.* FROM msg_threads
INNER JOIN messages ON messages.msg_thread_id = msg_threads.id
INNER JOIN recipiences ON recipiences.msg_thread_id = msg_threads.id
WHERE (SELECT COUNT(*) FROM `reads` WHERE reads.message_id = messages.id AND reads.user_id = #{self.id}) = 0
AND (SELECT COUNT(*) FROM recipiences WHERE recipiences.user_id = #{self.id} AND recipiences.msg_thread_id = msg_threads.id) > 0
")
end
end
Seems to be working fine!
Also to check if a specific thread (and message) are read:
class Message < ActiveRecord::Base
# stuff...
def read?(user_id)
Read.exists?(:user_id => user_id, :message_id => self.id)
end
end
class MsgThread < ActiveRecord::Base
# stuff...
def updated?(user_id)
updated = false
self.messages.each { |m| updated = true if !m.read?(user_id) }
updated
end
end
Any suggestions to improve this?
Add a named_scope to the MsgThread model:
class MsgThread < ActiveRecord::Base
named_scope :unread_threads, lambda { |user|
{
:include => [{:messages=>[:reads]}, recipiencies],
:conditions => ["recipiences.user_id = ? AND reads.message_id IS NULL",
user.id],
:group => "msg_threads.id"
}}
end
Note: Rails uses LEFT OUTER JOIN for :include. Hence the IS NULL check works.
Now you can do the following:
MsgThread.unread_threads(current_user)
Second part can be written as:
class Message
has_many :reads
def read?(usr)
reads.exists?(:user_id => usr.id)
end
end
class MsgThread < ActiveRecord::Base
def updated?(usr)
messages.first(:joins => :reads,
:conditions => ["reads.user_id = ? ", usr.id]
) != nil
end
end
You might want to take a look at Arel, which can help with complex SQL queries. I believe (don't quote me) this is already baked into Rails3.
Related
Having my db setup like this ("type" is always User although I have different kind of User via STI):
class User
# fields
# :id
# :sender_id, :sender_type
# :recipient_id, :recipient_type
end
Postcard model:
class Postcard < ActiveRecord::Base
belongs_to :owner, :class_name => User
belongs_to :recipient, :class_name => User
end
I'd like to setup the User model something like this:
class User < ActiveRecord::Base
has_many :postcards, :as => [:sender or :recipient] # this is not working
end
So I could say:
user.postcards
Is it possible?
PS: I've also tried this road:
has_many :postcards, :finder_sql => Proc.new { "SELECT * FROM postcards WHERE postcards.owner_id=#{id} OR postcards.recipient_id=#{id}" }
But found myself stuck on scopes as :finder_sql recreates a whole new SQL:
User.postcards.by_status('new').size
As mentioned by joelparkerhenderson I need to think differently my association strategy.
As I'd like to have:
user.postcards
My answer is simply to use scopes in Postcard model:
scope :of_user, lambda { |user| where("recipient_id = ? OR owner_id = ?", user.id, user.id) }
So I can invoke:
Postcard.of_user user
I could even wrap it in User model:
def postcards
Postcard.of_user self
end
I have the usual polymorphic associations for comments:
class Book < ActiveRecord::Base
has_many :comments, :as => :commentable
end
class Article < ActiveRecord::Base
has_many :comments, :as => :commentable
end
class Comment < ActiveRecord::Base
belongs_to :commentable, :polymorphic => true
end
I'd like to be able to define Book.recently_commented, and Article.recently_commented based on the created_at timestamp on the comments. Right now I'm looking at a pretty ugly find_by_SQL query to do this with nested selects. It seems as though there must be a better way to do it in Rails without resorting to SQL.
Any ideas? Thanks.
For what it's worth, here's the SQL:
select * from
(select books.*,comments.created_at as comment_date
from books inner join comments on books.id = comments.commentable_id
where comments.commentable_type='Book' order by comment_date desc) as p
group by id order by null;
Sometimes it's just best to add a field to the object of which you are commenting. Like maybe a commented_at field of datetime type. When a comment is made on an object, simply update that value.
While it is possible to use SQL to do it, The commented_at method may prove to be much more scalable.
Not sure what your method has looked like previously but I'd start with:
class Book < ActiveRecord::Base
def self.recently_commented
self.find(:all,
:include => :comments,
:conditions => ['comments.created_at > ?', 5.minutes.ago])
end
end
This should find all the books that have had a comment created on them in the last 5 minutes. (You might want to add a limit too).
I'd also be tempted to create a base class for this functionality to avoid repeating the code:
class Commentable < ActiveRecord::Base
self.abstract_class = true
has_many :comments, :as => :commentable
def self.recently_commented
self.find(:all,
:include => :comments,
:conditions => ['comments.created_at > ?', Time.now - 5.minutes])
end
end
class Book < Commentable
end
class Article < Commentable
end
Also, you might want to look at using a plugin to achieve this. E.g. acts_as_commentable.
I have the following models:
class Campaign < ActiveRecord::Base
has_many :campaign_keywords
has_many :leads, :through => :campaign_keywords
end
class CampaignKeyword < ActiveRecord::Base
belongs_to :campaign
has_many :leads
end
class Lead < ActiveRecord::Base
belongs_to :campaign_keyword
end
I am trying to build a function in the "Campaign" model that will only return leads which belong to a given campaign_keyword.
My attempt is:
def leads?(campaign_keyword_id = -1)
self.leads :conditions => ['campaign_keyword_id = #{campaign_keyword_id}']
end
but this does not work, the conditions are ignored.
Can you see a solution to this?
Create a named_scope for your Lead model, like so:
class Lead < ActiveRecord::Base
belongs_to :campaign_keyword
named_scope :with_keyword, lambda { |keyword| { :conditions => { :campaign_keyword => keyword } } }
end
Now, when you want to get leads for a particular campaign keyword, you would do so like this:
def leads_for_campaign(keyword)
self.leads.with_keyword(keyword)
end
This is much nicer and more re-usable, because the Lead model itself now knows how to find leads for a specific campaign.
For more of an idea of what you can do with named_scopes, check out http://apidock.com/rails/ActiveRecord/NamedScope/ClassMethods/named_scope
Try this:
def leads?(campaign_keyword_id = -1)
self.leads.all :conditions => ['campaign_keyword_id = #{campaign_keyword_id}']
end
I would rewrite your query as follows:
def leads?(campaign_keyword_id = -1)
self.leads.all :conditions => ['campaign_keyword_id = ?', campaign_keyword_id]
end
OR
self.leads.find_all_by_compaign_keyword_id(campaign_keyword_id)
I would like to list all posts that are connected with some specific category and classroom.
I have:
class Post < ActiveRecord::Base
has_many :category_posts
has_many :categories, :through => :category_posts
has_many :classroom_posts
has_many :classrooms, :through => :classroom_posts
end
class Category < ActiveRecord::Base
has_many :category_posts
has_many :posts, :through => :category_posts
end
class CategoryPost < ActiveRecord::Base
belongs_to :category
belongs_to :post
end
class Classroom < ActiveRecord::Base
has_many :classroom_posts
has_many :posts, :through => :classroom_posts
end
class ClassroomPost < ActiveRecord::Base
belongs_to :classroom
belongs_to :post
end
And I wanna do something like this
Post.where(["category.id = ? AND classroom.id = ?", params[:category_id], params[:classroom_id]])
It indeed is very simple task, but I don't know what I should be looking for (keywords).
It's the same problem like this, but in rails.
EDIT:
I added more details to the question.
This works, but only if I have both params specified. Witch is not always the case - I dont know what params would be specified.
Post.joins(:categories, :classrooms).where(["categories.id = ? AND classrooms.id = ?", params[:classroom_id], params[:category_id]])
Category.find(params[:category_id]).posts
Also take a look at the guides:
Guides for Rails 2.35
Guides for Rails 3.0
Here is what I would do in Rails 3:
In post.rb:
def self.in_category(category_id)
if category_id.present?
join(:category_posts).where(category_posts: {category_id: category_id})
else
self
end
end
def self.in_classroom(classroom_id)
if classroom_id.present?
join(:classroom_posts).where(classroom_posts: {classroom_id: category_id})
else
self
end
end
I do not join Classroom or Category since it makes more work for DBMS and this is not required.
Now, you can do:
Post.in_category(params[:category_id]).in_classroom(params[:classroom_id])
I haven't tested it though. So do not hesitated to ask if needed.
I think that should work:
Post.joins(:category_posts, :classroom_posts)
.where(
["category_posts.category_id = ?
AND classroom_posts.classroom_id = ?", params[:category_id], params[:classroom_id]])
This will traslate to a SQL like :
SELECT
p.*
FROM
posts AS p
INNER JOIN
category_posts AS cap ON cap.id = p.category_posts_id
INNER JOIN
classroom_posts AS clp ON clpid = p.classroom_posts_id
WHERE
cap.category_id = '1' AND clp.classroom_id = '1'
;
As to whether to use :include or joins on Post look at this answer on stackoverflow.
Sounds like you need an if statment.
if params[:category_id] && params[:classroom_id]
Post.joins(:categories, :classrooms).where("classrooms.id" => params[:classroom_id], "categories.id" => params[:category_id]])
elsif params[:category_id]
Category.find(params[:category_id]).posts
else
Classroom.find(params[:classroom_id]).posts
end
I am trying to calculate the average (mean) rating for all entries within a category based on the following model associations ...
class Entry < ActiveRecord::Base
acts_as_rateable
belongs_to :category
...
end
class Category < ActiveRecord::Base
has_many :entry
...
end
class Rating < ActiveRecord::Base
belongs_to :rateable, :polymorphic => true
...
end
The rating model is handled by the acts as rateable plugin, so the rateable model looks like this ...
module Rateable #:nodoc:
...
module ClassMethods
def acts_as_rateable
has_many :ratings, :as => :rateable, :dependent => :destroy
...
end
end
...
end
How can I perform the average calculation? Can this be accomplished through the rails model associations or do I have to resort to a SQL query?
The average method is probably what you're looking for. Here's how to use it in your situation:
#category.entries.average('ratings.rating', :joins => :ratings)
Could you use a named_scope or custom method on the model. Either way it would still require some SQL since, if I understand the question, your are calculating a value.
In a traditional database application this would be a view on the data tables.
So in this context you might do something like... (note not tested or sure it is 100% complete)
class Category
has_many :entry do
def avg_rating()
#entries = find :all
#entres.each do |en|
#value += en.rating
end
return #value / entries.count
end
end
Edit - Check out EmFi's revised answer.
I make no promises but try this
class Category
def average_rating
Rating.average :rating,
:conditions => [ "type = ? AND entries.category_id = ?", "Entry", id ],
:join => "JOIN entries ON rateable_id = entries.id"
end
end