I'm learning scopes in Rails and having some trouble making some scope definitions. Say I have the following models..
class Category < ActiveRecord::Base
has_many :posts
has_many :comments, :through => :post
end
class Post < ActiveRecord::Base
belongs_to :category
has_many :comments
end
class Comment < ActiveRecord::Base
belongs_to :post
has_many :categories, :through => :post
# attribute: owner
end
Notice the attribute owner for Comment. I'm trying to write a scope which will return me the categories that have comments made by only the owner I pass. So if a category has comments and these comments are made by several other owners, these should not be included. I have this working somewhat. I'm getting categories that have comments with the owner I pass..but I also get categories that have comments by other owners too.
In my Category model, I have this..
scope :comments_by_owner, lambda { |name| joins(:comments).where("comments.owner = ?", name) }
I make a call using
Category.comments_by_owner("tom")
I tried playing around with joins and uniq but no luck..
Try this:
scope :comments_by_owner, lambda { |name| joins(:posts => :comments).where(:comments => {:owner => name})
It appears that select with a block uses Array#select. Try running through all categories, selecting categories which contain no comments for other names.
def find_single_name_categories(name)
Category.scoped.select do |category|
category.comments.scoped.select { |comment| comment.name != name }.empty?
end
end
Related
I'm currently adjusting fedena to have a many:many relationship between students and guardians (as opposed to one:many student:guardians).
So this is what I did:
class Guardian < ActiveRecord::Base
has_many :parentings, :dependent=>:destroy
has_many :students, :through=>:parentings
end
class Student < ActiveRecord::Base
has_many :parentings, :dependent=>:destroy
has_many :guardians, :through=>:parentings
end
class Parenting < ActiveRecord::Base
attr_accessible :student_id, :guardian_id
belongs_to :student
belongs_to :guardian
end
inside guardian.rb there was this class method:
def self.shift_user(student)
# find all the guardians having a ward_id = student.d (comment my own)
self.find_all_by_ward_id(student.id).each do |g|
..
end
I want to change it using the newly defined relationshop ie
self.find_all_by_student_id(student.id).each do |g|
..
It doesn't work! I thought it would work since I've already defined that a Guardian has many students through the Parenting class.. I've tried several permutations of the command above and I keep on getting the error:
undefined method `find_all_by_student_id' for #<Class:0x1091c6b28>
ideas? I'm using ruby 1.8.7 and RoR 2.3.5
Guardian has no propety student_id so there is no method find_all_by_student_id. So I don't understand why you are confused. Why don't you just use student.guardians?
You can do this using a named scope and a more complex query
class Guardian < ActiveRecord::Base
has_many :parentings, :dependent=>:destroy
has_many :students, :through=>:parentings
named_scope :find_all_by_student_id, lambda {|student_id|
{ :all,
:select => "guardians.*",
:joins => "JOIN parentings ON parentings.guardian_id = guardians.id
JOIN students ON students.id = parentings.student_id",
:conditions = ["students.id = ?", student_id] } }
end
class Newsroom < ActiveRecord::Base
has_many :blog_posts
has_many :quote_posts
end
class BlogPost < ActiveRecord::Base
belongs_to :newsroom
end
class QuotePost < ActiveRecord::Base
belongs_to :newsroom
end
I would like to have an instance method, such that I could do #newsroom.posts to get a collection of blog_posts and quote_posts sorted by created_at.
def posts
#posts ||= #load and sort blog_posts, quote_posts, etc
end
What is the best and most efficient way to accomplish this? I have looked into using default_scope, something like:
default_scope :include => [:blog_posts, :quote_posts]
def posts
#posts ||= [blog_posts + quote_posts].flatten.sort{|x,y| x.created_at <=> y.created_at}
end
But I would rather keep the sorting at the database level, if possible. Any suggestions on how to accomplish this? Thanks.
Try something like this:
#app/models/newsroom.rb
scope :ordered_posts, lambda {
includes(:blog_posts,:quote_posts) & BlogPost.order("created_at asc") & QuotePost.order("created_at asc")
}
ARel should be able to handle the ordering of included Quote and Blog Posts. You could clean that up slightly by having scopes in both the BlogPost and QuotePost model that order by created_at and then use those scopes in the Newsroom#ordered_posts method.
I ended up using a polymorphic post model. This seems to give me what I want with the insignificant downside of having an extra model/table. I used delegate to hand off specific attribute getter methods to the correct model.
class Newsroom < ActiveRecord::Base
has_many :posts
end
class Post < ActiveRecord::Base
belong_to :blog_post, :polymorphic => true
delegate :title, :author, :etc, :to => :postable
end
class BlogPost < ActiveRecord::Base
has_one :post, :as => :postable
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 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 have a data model something like this:
# columns include collection_item_id, collection_id, item_id, position, etc
class CollectionItem < ActiveRecord::Base
self.primary_key = 'collection_item_id'
belongs_to :collection
belongs_to :item
end
class Item < ActiveRecord::Base
has_many :collection_items
has_many :collections, :through => :collection_items, :source => :collection
end
class Collection < ActiveRecord::Base
has_many :collection_items, :order => :position
has_many :items, :through => :collection_items, :source => :item, :order => :position
end
An Item can appear in multiple collections and also more than once in the same collection at different positions.
I'm trying to create a helper method that creates a menu containing every item in every collection. I want to use the collection_item_id to keep track of the currently selected item between requests, but I can't access any attributes of the join model via the Item class.
def helper_method( collection_id )
colls = Collection.find :all
colls.each do |coll|
coll.items.each do |item|
# !!! FAILS HERE ( undefined method `collection_item_id' )
do_something_with( item.collection_item_id )
end
end
end
I tried this as well but it also fails with ( undefined method `collection_item' )
do_something_with( item.collection_item.collection_item_id )
Edit: thanks to serioys sam for pointing out that the above is obviously wrong
I have also tried to access other attributes in the join model, like this:
do_something_with( item.position )
and:
do_something_with( item.collection_item.position )
Edit: thanks to serioys sam for pointing out that the above is obviously wrong
but they also fail.
Can anyone advise me how to proceed with this?
Edit: -------------------->
I found from online documentation that using has_and_belongs_to_many will attach the join table attributes to the retreived items, but apparently it is deprecated. I haven't tried it yet.
Currently I am working on amending my Collection model like this:
class Collection < ActiveRecord::Base
has_many :collection_items, :order => :position, :include => :item
...
end
and changing the helper to use coll.collection_items instead of coll.items
Edit: -------------------->
I've changed my helper to work as above and it works fine - (thankyou sam)
It's made a mess of my code - because of other factors not detailed here - but nothing that an hour or two of re-factoring wont sort out.
In your example you have defined in Item model relationship as has_many for collection_items and collections the generated association method is collection_items and collections respectively both of them returns an array so the way you are trying to access here is wrong. this is primarily case of mant to many relationship. just check this Asscociation Documentation for further reference.
do_something_with( item.collection_item_id )
This fails because item does not have a collection_item_id member.
do_something_with( item.collection_item.collection_item_id )
This fails because item does not have a collection_item member.
Remember that the relation between item and collection_items is a has_many. So item has collection_items, not just a single item. Also, each collection has a list of collection items. What you want to do is probably this:
colls = Collection.find :all
colls.each do |coll|
coll.collection_items.each do |collection_item|
do_something_with( collection_item.id )
end
end
A couple of other pieces of advice:
Have you read the documentation for has_many :through in the Rails Guides? It is pretty good.
You shouldn't need the :source parameters in the has_many declarations, since you have named your models and associations in a sensible way.
I found from online documentation that using has_and_belongs_to_many will attach the join table attributes to the retreived items, but apparently it is deprecated. I haven't tried it yet.
I recommend you stick with has_many :through, because has_and_belongs_to_many is more confusing and doesn't offer any real benefits.
I was able to get this working for one of my models:
class Group < ActiveRecord::Base
has_many :users, :through => :memberships, :source => :user do
def with_join
proxy_target.map do |user|
proxy_owner = proxy_owner()
user.metaclass.send(:define_method, :membership) do
memberships.detect {|_| _.group == proxy_owner}
end
user
end
end
end
end
In your case, something like this should work (haven't tested):
class Collection < ActiveRecord::Base
has_many :collection_items, :order => :position
has_many :items, :through => :collection_items, :source => :item, :order => :position do
def with_join
proxy_target.map do |items|
proxy_owner = proxy_owner()
item.metaclass.send(:define_method, :join) do
collection_items.detect {|_| _.collection == proxy_owner}
end
item
end
end
end
end
Now you should be able to access the CollectionItem from an Item as long as you access your items like this (items.with_join):
def helper_method( collection_id )
colls = Collection.find :all
colls.each do |coll|
coll.items.with_join.each do |item|
do_something_with( item.join.collection_item_id )
end
end
end
Here is a more general solution that you can use to add this behavior to any has_many :through association:
http://github.com/TylerRick/has_many_through_with_join_model
class Collection < ActiveRecord::Base
has_many :collection_items, :order => :position
has_many :items, :through => :collection_items, :source => :item, :order => :position, :extend => WithJoinModel
end