I have a need in my app to allow users to bookmark a post. They should only be able to create one bookmark per post. I've set up my polymorphic association like so:
class Post < ActiveRecord::Base
has_many :bookmarks, :as => :bookmarkable
end
class Bookmark < ActiveRecord::Base
belongs_to :bookmarkable, :polymorphic => true
belongs_to :user
end
class User < ActiveRecord:Base
has_many :posts
has_many :bookmarks
end
In my view, a user can create a bookmark. I would like to find some way to replace the "Create Bookmark" view code with "Delete Bookmark" code, if the user has already bookmarked a particular post.
If I try to do something like this:
#post = Post.find(params[:id, :include => [:bookmarks]])
- if #post.bookmarks.users.include?(#user)
I get a No Method error for "users"
How can I access owners of the bookmarks, to determine if the current user has already bookmarked a page?
Thank you.
I would approach this from the user's point of view:
class User < ActiveRecord::Base
has_many :posts
has_many :bookmarks
# Rails 3
def bookmarked?(post)
bookmarks.where(
{
:bookmarkable_id => post.id, :bookmarkable_type => post.class.name
}
).count > 0
end
# Rails 2
def bookmarked?(post)
bookmarks.find(:all, :conditions =>
{
:bookmarkable_id => post.id, :bookmarkable_type => post.class.name
}
).count > 0
end
end
if #user.bookmarked?(#post)
# Show delete link
else
# Show bookmark link
end
I would also advice you to add a validation to your bookmarks model that prevents a user from bookmarking the same post twice.
Related
I'm trying to set up a classic 'like' model for Posts on a blog, where users can create one Like for any Post. I have the following models:
class Post < ApplicationRecord
belongs_to :user
has_many :likes
end
class User < ApplicationRecord
has_many :posts
has_many :likes
end
class Like < ApplicationRecord
belongs_to :user
belongs_to :post, counter_cache: true
end
In my controller I monitor the currently logged in user, current_user.
I would like to add a column to my Posts model that indicates whether or not current_user has liked each Post.
I tried adding a method to the Posts model that looks for likes:
class Post < ApplicationRecord
belongs_to :user
has_many :likes
def user_liked
!likes.empty?
end
end
And using includes in the controller method.
#posts = Post.includes(likes: { user: current_user }).where(safe_params).order(order)
render json: #posts
However I get the following error:
ArgumentError (#<User id: 1, username: "pete", ... > was not recognized for preload):
app/controllers/posts_controller.rb:51:in `index'
I'm using Rails API 5.0.
Update:
To clarify, I'm looking for the Rails equivalent of this SQL statement:
SELECT *
FROM Posts
LEFT OUTER JOIN
(SELECT *
FROM Likes
WHERE Likes.user_id = current_user.id) AS MyLikes
ON Posts.id = MyLikes.post_id
The problem is includes takes the name of associations as parameters (like :user, :likes, :posts) as parameter and does not include any instances(In your case current_user)
You could try the following instead.
#posts = Post.includes(likes: :user).where(safe_params).order(order)
If you want to check if posts belong to current_user or not (in haml view for example)
- #posts.each do |post|
- if post.likes && post.likes.any? { |like| like.user_id == current_user.id }
%span You have commented on this post
I have a discussion forum where users can see a list of unread posts. The way I'm doing this is to use a Look, User and Post model:
class Look < ActiveRecord::Base
belongs_to :post
belongs_to :user
end
class User < ActiveRecord::Base
has_many :posts, through: :looks
has_many :looks
end
class Post < ActiveRecord::Base
belongs_to :user
has_many :looks
has_many :users, through: :looks
end
So the way this works is that there is a list of all post IDs a user has viewed. It's created through the 'show' method:
def show
if current_user
viewer = current_user
view_ids = viewer.posts.pluck(:id).uniq
not_viewed = Post.where("id not in (?)", view_ids)
not_viewed_ids = not_viewed.pluck(:id)
unless Post.find(params[:id]).in?(not_viewed_ids)
Look.create(user: current_user, post: #post, viewstamp: Time.now)
end
end
end
This all works fine so far. The problem is I want to create a Look for all posts, so that I can essentially 'mark all as read'. This line works fine for creating a Look for the current post:
unless Post.find(params[:id]).in?(not_viewed_ids)
Look.create(user: current_user, post: #post, viewstamp: Time.now)
end
...but how do I make one that creates a Look for every post? Like this:
Look.create(user: current_user, post: [NEED ARRAY OF POSTS HERE], viewstamp: Time.now)
The reason I want to do this is so a user can mark all posts as read.
You can create the Look automatically just by adding the users to the posts.
Post.all.each { |p| p.users << current_user; p.save }
I want to limit the amount of records a user can add to the database.
I'm not sure of the 'Rails' way to go about this...
I am using Devise and thought of creating a custom validation method but you can't access current_user from within a model and it isn't correct.
How can I do this from the controller and still return an error message to my users?
I had this
validate :post_count
def post_count
current = current_user.posts.count
limit = current_user.roles.first.posts.count
if current > limit
errors.add(:post, "Post limit reached!")
end
end
but it isn't the correct way to go about it as it would be hacky to get the current_user into the model
You could do something like this:
class User < ActiveRecord::Base
has_many :domains
has_many :posts, through: :domains
def post_limit
roles.first.posts.count
end
end
class Domain < ActiveRecord::Base
belongs_to :user
has_many :posts
end
class Post < ActiveRecord::Base
belongs_to :domain
delegate :user, to: :domain
validate :posts_count_within_limit, on: :create
def posts_count_within_limit
if self.user.posts(:reload).count >= self.user.post_limit # self is optional
errors.add(:base, 'Exceeded posts limit')
end
end
end
Based on this answer.
I have the following models:
class User < ActiveRecord::Base
has_many :survey_takings
end
class SurveyTaking < ActiveRecord::Base
belongs_to :survey
def self.surveys_taken # must return surveys, not survey_takings
where(:state => 'completed').map(&:survey)
end
def self.last_survey_taken
surveys_taken.maximum(:position) # that's Survey#position
end
end
The goal is to be able to call #user.survey_takings.last_survey_taken from a controller. (That's contrived, but go with it; the general goal is to be able to call class methods on #user.survey_takings that can use relations on the associated surveys.)
In its current form, this code won't work; surveys_taken collapses the ActiveRelation into an array when I call .map(&:survey). Is there some way to instead return a relation for all the joined surveys? I can't just do this:
def self.surveys_taken
Survey.join(:survey_takings).where("survey_takings.state = 'completed'")
end
because #user.survey_takings.surveys_taken would join all the completed survey_takings, not just the completed survey_takings for #user.
I guess what I want is the equivalent of
class User < ActiveRecord::Base
has_many :survey_takings
has_many :surveys_taken, :through => :survey_takings, :source => :surveys
end
but I can't access that surveys_taken association from SurveyTaking.last_survey_taken.
If I'm understanding correctly you want to find completed surveys by a certain user? If so you can do:
Survey.join(:survey_takings).where("survey_takings.state = 'completed'", :user => #user)
Also it looks like instead of:
def self.surveys_taken
where(:state => 'completed').map(&:survey)
end
You may want to use scopes:
scope :surveys_taken, where(:state => 'completed')
I think what I'm looking for is this:
class SurveyTaking < ActiveRecord::Base
def self.surveys_taken
Survey.joins(:survey_takings).where("survey_takings.state = 'completed'").merge(self.scoped)
end
end
This way, SurveyTaking.surveys_taken returns surveys taken by anyone, but #user.survey_takings.surveys_taken returns surveys taken by #user. The key is merge(self.scoped).
Waiting for further comments before I accept..
I have 3 models User,Listing and Message. What I want is for an authenticated user to have many listings. The listings then can have multiple messages. So the messages are tied to the user through the listing model. I am able to get a users listings but not able to get the users messages which he owns through the listings. Here are the associations that I currently have.
class User < ActiveRecord::Base
has_many :listings, :dependent => :destroy
end
class Listing < ActiveRecord::Base
belongs_to :user
has_many :messages
end
class Message < ActiveRecord::Base
belongs_to :listing
end
To create a message I simply do this;
#listing = Listing.find(params[:listing_id])
#message = #listing.messages.build(params[:message])
And getting the user's listing i have this;
#user_listings = Listing.user_listings(current_user)
But getting the messages tied to the user's listings proves to be elusive. What am I doing wrong or how do I go about this? help appreciated.
Still not sure where user_listings comes from but why not this:
#user = User.find(params[:user_id], :include => {:listings => :messages})
#user.listings.each do |listing|
listing.messages.each do |message|
#or
#user.listings.collect(&:messages).each do |message|
#or (just read about using authenticated user so the same as above like this
current_user.listings(:all, :include => :messages)...
Include prefetches all the listings' associated messages in one query in order that they're not fetched in the loop causing n+1 querying.
----------
Or another approach, if you don't need the listings data.
#messages.rb
def self.user_messages user_id
find(:all, :joins => :listings, :conditions => ["listings.user_id = ?", user_id])
#with pagination
def self.user_messages user_id, page
paginate(:all, :joins => :listings,
:conditions => ["listings.user_id = ?", user_id],
:per_page => 10, :page => page)
updated regarding your comment.
You may want to just add has_many :messages to the user class as well and add a user_id column to Message. Then you could just do current_user.messages
How about something like this:
class User < ActiveRecord::Base
has_many :listings, :dependent => :destroy
has_many :listing_messages, :through => :listings
That way you dont have to "tie" the messages with the user because it is always accessed through the listing association:
current_user.listing_messages.all
Or have I misunderstood your question?
If you have current_user pulled already. You can just access listings directly by calling
current_user.listings
instead of
#user_listings = Listing.user_listings(current_user)