Two has_many associations to the same model - ruby-on-rails

I have a blog model that has many posts and some of these posts are top posts. My intention is to have something like the following.
class Blog
has_many :posts
has_many :top_posts, class_name: 'Post'
end
class Post
belongs_to :blog
end
As you can see, posts and top posts are the same objects, but the collection of top posts is different from the collection of posts. Some posts are top posts too, but others not.
The problem is when I try to do blog.top_posts, it returns the same collection as blog.posts, which are all posts from that blog. I want blog.top_posts to return only the posts I have associated as the blog's top posts via blog.top_post_ids << random_post. Thanks in advance!

I will assume that , like David asked you in comment , you have particular post.
And a post could become a top_post if , by example , he has 50 likes , or 1000 views , or wathever attributes persistent in your database making a post become a top post .
If you don't have any criteria persistent on your database , you can't use active record to gets top_post.
Anyway if you have , you should use a scope :
class Blog
has_many :posts
end
class Post
belongs_to :blog
scope :top_posts, -> { where(views > 1000 ) } #or any conditions base on your criterias
end
And you can get them simply :
blog = Blog.first
blog.posts.top_posts # => [top posts belonging to this blog]
it's an answer based on assumption ...
doc for scope : http://guides.rubyonrails.org/active_record_querying.html#scopes
s

The problem with using the has_many is it simply attaches the ID of your Blog object to each Post object so when you call blog.posts or blog.top_posts it executes a SQL query looking for Posts WITH id=Blog.id thus, you get the same list twice.
I would suggest you have a single has_many posts and then sort the list that is returned by whatever makes each post the "Top Post." Or, if you'd like to avoid sorting I'd suggest something like this:
class Blog
has_many :posts
def initialize
#top_posts = []
end
def add_top_post(post)
if self.posts.include?(post)
#top_posts << post
end
end
end
class Post
belongs_to :blog
end

How about a scope?
http://guides.rubyonrails.org/active_record_querying.html#scopes
class Blog
has_many :posts
end
class Post
belongs_to :blog
scope :top, -> { where top_post: true }
end
# this gets all the top posts
blog.posts.top

Related

How to access the other relation in Model.joins(:some_association)

Let's say I have two models, Post and Comment. Post has_many Comments.
class Post < ActiveRecord::Base
has_many :comments
end
class Comment < ActiveRecord::Base
belongs_to :post
end
Suppose I want to get a list of Post, Comment pairs. I could do this:
Post.includes(:comments).each do |post|
post.comments.each do |comment|
# do something with post and comment
end
end
But let's say that for whatever reason, maybe to do one less database query, I do a join:
Post.joins(:comments)
# => [#<Post:0xblahblah>]
Given one of the Posts returned by the above query, how do I access the Comment that the Post was joined with?
ps: for eager load, use includes instead of joins. You can access a certain post comments directly, by post.comments; or access a comment post by comment.post

Tags per post per user, the user isn't listed.

My main models are that I have users and I have recipes.
I'm trying to implement a tagging structure such that each user can tag a recipe with individual tags. So when viewing a recipe, they would only see tags that they themselves have added.
I created two models hashtags, and hashtagging that is the join table. It is set up as so:
models/hashtags.rb
class Hashtag < ActiveRecord::Base
has_many :hashtaggings
has_many :recipes, through: :hashtaggings
has_many :users, through: :hashtaggings
end
models/hashtagging.rb
class Hashtagging < ActiveRecord::Base
belongs_to :user
belongs_to :hashtag
belongs_to :recipe
end
models/recipe.rb
class Recipe < ActiveRecord::Base
...
has_and_belongs_to_many :users
has_many :hashtaggings
has_many :hashtags, through: :hashtaggings
....
def all_hashtags=(name)
self.hashtags = name.split(",").map do |name|
Hashtag.where(name: name.strip).first_or_create!
end
end
def all_hashtags
self.hashtags.map(&:name).join(",")
end
end
class User < ActiveRecord::Base
...
has_many :hashtaggings
has_many :hashtags, through: :hashtaggings
...
end
This works great for creating the hash tags however I'm at a loss for how to incorporate the user aspect of it. How when assigning the tags can I also assign the current user to those tags and then just return those?
There are two steps, creation and display...
Creation
This one is going to be tricky, because you can't simply do something like...
#recipe.hashtags.create(name: "Tasty as heck!")
...because neither the recipe, nor the hashtag, knows anything about the user. It's a two-step process.
class RecipeHashtagsController
def create
current_hashtag = Hashtag.find_or_create_by(name: "Scrumptious!")
current_recipe = Recipe.find(params[:recipe_id])
hashtagging = Hashtagging.find_or_create_by(hashtag: current_hashtag, user: current_user, recipe: current_recipe)
# redirect_to somewhere_else...
end
end
A few things I did there:
I'm using find_or_create_by since I'm assuming you don't want either duplicate hashtags or duplicate hashtaggings. You could also just create.
I'm assuming you have some kind of current_user method in ApplicationController or through a gem like Devise.
I'm assuming you have a controller like RecipeHashtags, and a nested resource that matches and will provide an id from the route. I recommend nesting here since you aren't simply creating a hashtag, but you are creating a hashtag within the specific context of a recipe.
Displaying
This gets tricky, because you want to display recipe.hashtags but with a condition on the join table hashtaggings. This is not super straightforward.
What I'm thinking is you might want to be able to do something like...
#recipe.hashtags_for_user(current_user)
...which could be in the form of a method on Recipe.
class Recipe
def hashtags_for_user(user)
Hashtags.joins(:hashtaggings).where(hashtaggings: { user_id: user.id, recipe_id: self.id })
end
end
You can read more about the hash inside the .where call in the Active Record Querying Rails Guide (check out section 12.3).
Edit: The Controller
I recommend creating RecipeHashtags as a nested route pointing to separate controller, since the creation of a hashtag is dependent on which recipe it's being created for.
routes.rb
resources :recipes do
resources :hashtags, only: [:create]
end
...which will show something like the following when you do rake routes in the terminal...
POST /recipes/:recipe_id/hashtags(.:format) recipe_hashtags#create
Note: I'm assuming you have a resource for resource for recipes. If you don't, this may duplicate some routes and have other, unintended results.
The default behavior for Rails is to assume you've got a controller like recipe_hashtags_controller based on how you defined your resources. You can always override this if you like.

Rails has_many sums and counts with ActiveRecord

I've got a dilemma I think I may have coded myself into a corner over. Here's the setup.
My site has users. Each user has a collection of stories that they post. And each story has a collection of comments from other users.
I want to display on the User's page, a count of the total number of comments from other users.
So a User has_many Stories, and a Story has_many comments.
What I tried was loading all the users stories in #stories and then displaying #stories.comments.count, but I get undefined method 'comments' when I try to do that. Is there an efficient ActiveRecord way to do this?
class User < ActiveRecord::Base
has_many :stories
has_many :comments, through: :stories
end
class Story < ActiveRecord::Base
belongs_to :user
has_many :comments
end
class Comment < ActiveRecord::Base
belongs_to :story
end
Now you should be able to get User.last.comments.count
I think you need to refine this more for a proper labelling.
The quick solution is to iterate over the #stories collection and add the counts up. This is not a purely active record solution though.
totalComments = 0
#stories.each do |story|
totalComments += story.count
end
For a pure active record solution I would need to assume that each has_many association has a corresponding belongs_to association. So a User has_many Stories and a Story belongs_to a User. If that is the case and comments have a similar association to stories then you can search comments by user_id. Something like:
Comments.where("comment.story.user" => "userId")
I hope that helps.
In your controller you should have something like this (note the use of includes):
#user = User.find( params[:id] )
#stories = #user.stories.includes(:comments)
Then in your view you can do something like the following to show the total number of comments for that particular user:
Total number of comments: <%= #stories.map{|story| story.comments.length}.sum %>

Making record available only to certain models in Rails 3

I have a weird design question. I have a model called Article, which has a bunch of attributes. I also have an article search which does something like this:
Article.project_active.pending.search(params)
where search builds a query based on certain params. I'd like to be able to limit results based on a user, that is, to have some articles have only a subset of users which can see them.
For instance, I have an article A that I assign to writers 1,2,3,4. I want them to be able to see A, but if User 5 searches, I don't want that user to see. Also, I'd like to be able to assign some articles to ALL users.
Not sure if that was clear, but I'm looking for the best way to do this. Should I just store a serialized array with a list of user_id's and have -1 in there if it's available to All?
Thanks!
I would create a join table between Users and Articles called view_permissions to indicate that a user has permission to view a specific article.
class ViewPermission
belongs_to :article
belongs_to :user
end
class User
has_many :view_permissions
end
class Article
has_many :view_permissions
end
For example, if you wanted User 1 to be able to view Article 3 you would do the following:
ViewPermission.create(:user_id => 1, :article_id => 3)
You could then scope your articles based on the view permissions and a user:
class Article
scope :viewable_by, lambda{ |user| joins(:view_permissions).where('view_permissions.user_id = ?', user.id) }
end
To search for articles viewable by a specific user, say with id 1, you could do this:
Article.viewable_by(User.find(1)).project_active.pending.search(params)
Finally, if you want to assign an article to all users, you should add an viewable_by_all boolean attribute to articles table that when set to true allows an article to be viewable by all users. Then modify your scope to take that into account:
class Article
scope :viewable_by, lambda{ |user|
joins('LEFT JOIN view_permissions on view_permissions.article_id = articles.id')
.where('articles.viewable_by_all = true OR view_permissions.user_id = ?', user.id)
.group('articles.id')
}
end
If an Article can be assigned to multiple Writers and a Writer can be assigned to multiple Articles, I would create an Assignment model:
class Assignment < AR::Base
belongs_to :writer
belongs_to :article
end
Then you can use has_many :through:
class Article < AR::Base
has_many :assignments
has_many :writers, :through => :assignments
end
class Writer < AR::Base
has_many :assignments
has_many :articles, :through => :assignments
end

Can I access the id of the parent object inside a named_scope when fetching associated objects with the 'others' method?

Let's say you have two models: articles and comments.
class Article < ActiveRecord::Base
has_many :comments
end
You know you can fetch associated comments to an article like this:
article = Article.first
article.comments # => SELECT * FROM "comments" WHERE ("comments".article_id = 123)
Is there a way to explicitly access the article_id (123) within a named_scope?
I need this for a complex named_scope that joins another table. Basically the named_scope will depend on to be called from the associated parent object to make sense (article.comments.my_named_scope and not Comments.my_named_scope).
I don't want to pass the id as a parameter for the named_scope. So, instead of passing the article_id to the named scope with ... lambda { |article| ...} and access the id with "... #{article.id} ...", I want to somehow access this article_id that the others method uses, which I get from the has_many association.
Sounds like what you're actually after is an association extension:
http://guides.rubyonrails.org/association_basics.html#association-extensions
In particular, proxy_owner, which will be the #article in question
eg:
class Article < ActiveRecord::Base
has_many :posts do
def sample_extension
puts "Proxy Owner #{proxy_owner}"
end
end
end
#article.posts.sample_extension
Been struggling with the same issue. You can try this, which is a more elegant than using association extensions:
class Article < ActiveRecord::Base
has_many :posts
end
class Post < ActiveRecord::Base
def self.get_article_id
self.new.article_id
end
end
#article = Article.new
#article.posts.get_article_id
Within class methods for Post, you can now just use get_article_id anywhere you need the ID of the parent article. With proxy associations, I wasn't able to do that.
I like #ajkochanowicz's solution but looks like there's a DB hit involved there (Rails 3.2.x), so just a heads up, not really ideal considering the fact that you already have the parent object on hand somewhere.
For Rails 4 and above
The newer way to do it in Rails4+ is:
class Article < ActiveRecord::Base
has_many :comments do
def my_named_scope
puts "Scope Owner = #{#association.owner}"
end
end
end
article = #article.comments.my_named_scope
Inside the scope my_named_scope, #association.owner returns the Article object that .comments was called on. Hence the article returned by the code above is same as the #article object.
Alternative method
If you don't want to use extensions and would rather avoid the "create a new object and get id from there" method (as described by Chanpory's answer), here is how to do it:
class Article < ActiveRecord::Base
has_many :comments
end
class Comment < ActiveRecord::Base
def self.get_article_id
Comment.scope_attributes["article_id"] # scope_attributes returns a hash of all the attributes inherited from the owner of this scope
end
end
#article = Article.find(10)
#article.comments.get_article_id # returns 10

Resources