OhDaeSu OhDaeSu - 17 days ago 6
Ruby Question

Rails: User has many posts, but only one post can be stickied/pinned

A user should be able to pin a post/create a sticky post. But he should only be allowed to pin one post only. So if he decides to create a new pinned post/pin another post, he has to "un-pin" the old one (and an error message appears).

There is a simple association between my user and post model and the post model has a column called "pinned" which is a boolean (true or false). This is what I've tried:

models/user.rb

class User < ApplicationRecord
has_many :posts, dependent: :destroy
validates_uniqueness_of :posts, if: :only_one_pinned_post

def only_one_pinned_post
if self.post.where(pinned: true).size == 1
true
else
false
end
end
end


models/post.rb

class Post < ApplicationRecord
belongs_to :user
end


My questions:


  • How can I make it work that the user is only allowed to pin one post?

  • Where do I put my error message, if he already has a pinned post but wants to create another?


Answer

Since you want to ensure that there is only on pinned post, you might want to add the validation to the Post model:

class Post < ActiveRecord::Base
  belongs_to :user

  scope :pinned,  -> { where(pinned:true) }
  scope :without, ->(id) { where.not(id: id) if id }

  validate :only_one_pinned_post_per_user

private

  def only_one_pinned_post_per_user
    if pinned? && user.posts.pinned.without(id).any?
      errors.add(:pinned, 'Another post ist already pinned')
    end
  end
end

I wonder if it is better (from a usability point of view) to implementing it differently: Perhaps you should not tell the user that he cannot pin more than one post. Instead you can just save the newly pinned post and unpin any other post automatically. This could be done with a after save callback:

class Post < ActiveRecord::Base
  belongs_to :user

  scope :pinned,  -> { where(pinned:true) }
  scope :without, ->(id) { where.not(id: id) if id }

  after_save :ensure_only_one_pinned_post

private

  def ensure_only_one_pinned_post
    user.posts.pinned.without(id).update_all(pinned: false) if pinned?
  end
end

The without scope is used to not find and not count posts with the given id. I use this in this case to ensure that the currently created or updated post is not found nor counted when looking for other already pinned posts.