rails: deleting objects in has_many through only if no longer referenced - ruby-on-rails

A posts model:
class Post < ActiveRecord::Base
has_many :taggings, dependent: :destroy
has_many :tags, through: :taggings
and a tags model:
class Tag < ActiveRecord::Base
has_many :taggings
has_many :posts, through: :taggings
and the associated join table:
class Tagging < ActiveRecord::Base
belongs_to :tag, dependent: :destroy
belongs_to :post
so, the dependent: :destroy in Post makes the db entries in Tagging to be destroyed when doing for instance Post.last.destroy, and the dependent: :destroy in Tagging makes the associated Tag object to be destroyed as well. Fine.
My problem is, 2 posts may share the same Tag, and I would like to destroy that tag -only- if the post being deleted is the last one referencing it. I don't allow duplicate entries in the Tag table. The tags for a post are submitted as a a string with the tags separated by commas, and upon creation of a post I do the following:
def tag_names=(names)
self.tags = names.split(",").map{ |tag| Tag.where(name: tag.squish).first_or_create! }
end
Any idea on how I could achieve this? Thanks!

Are you using a plugin? act_as_taggable or something like that?
If so, take a look to the docs because probably is already implemented.
If not, you can always implement the after_destroy callback to count the elements associated to a tag and delete the tag in the case that you have "empty" tags.
For instance, in your Post model:
class Post
before_destroy :clean_up_tags
protected
def clean_up_tags
tags_to_delete = Tagging.where(id: self.tag_ids).group(:tag_id).having("count(distinct taggable_id) = 1").pluck(:id)
Tag.find(tags_to_delete).map(&:destroy)
end
end
This method is assuming that you have a method tag_ids that returns the tags associated to an specific Post and that your taggings model is polymorphic).
As you probably have more than one model with the tags feature, a good approach would be packing this method into a module and include it in all the models, in this way you keep the things DRY.

Related

Find record with two ids at the same time

I have models:
User
class User < ActiveRecord::Base
has_many :users_conversations, dependent: :destroy
has_many :conversations, through: :users_conversations, dependent: :destroy
end
UsersConversation
class UsersConversation < ActiveRecord::Base
belongs_to :conversation
belongs_to :user
end
Conversation
class Conversation < ActiveRecord::Base
has_many :users_conversations, dependent: :destroy
has_many :users, through: :users_conversations
end
I need to create query to get the joint conversation between two known users by their ID's. I have this one
Conversation.left_joins(:users).where(users: { id: [current_user.id, params[:user_id]]}).first
but it finds conversations where is any of [current_user.id, params[:user_id]], so I always get the same conversation, where is current_user, but it is wrong. As I see it is because id: [current_user.id, params[:user_id]] works like OR, but I need AND. Thank you.
The answer is not correct as mentioned in the comments. Please do not refer it.
Try using
Conversation.left_joins(:users).find_by("users.id IN(?)", [current_user.id, params[:user_id]]})
Haven't actually tried but this should work. Please let me know if you face an error.
Also, use find_by instead or Model.where(condition).first. Using the latter is a bad habit and convention in rails.
Explanation: When you have to find the records where a field matches an array, i.e., you need the value of this filed to be specific from an array, you should use IN operator of SQL. Like
SELECT * FROM Customers WHERE Country IN ('Germany', 'France', 'UK');

Conditions on has_many in Rails 4.2?

I have a Post model that has_many :authors and has_many :tags through :tag_posts, an Author model that has_many :posts and has_many :tags through :posts, and a Tag model that has_many :posts through :tag_posts.
Posts have to pass moderation (stored as an enum Post.status) before they're visible on the site. Moderators can see unmoderated posts by looking for ones with a different Post.status, but regular users should never be able to see them.
Is there any way I can filter all these associations to make sure they only return the posts that moderators have approved?
For example: if I call Author#tags now, that returns all the tags on all the posts that author has written, but I only want it to return the tags on the approved posts that the author has written. Say the author has two posts, the first is approved and is tagged 'octopus', and the second hasn't been approved yet and is tagged 'squid': calling #tags on that author should only return the octopus tag, not the squid tag.
I have a Post model that has_many :authors and has_many :tags through :tag_posts, an Author model that has_many :posts and has_many :tags through :posts, and a Tag model that has_many :posts through :tag_posts.
First thing's first, let's tidy up those relations. I believe this is what you really want:
class Post < ActiveRecord::Base
belongs_to :author # NOT has_many
has_and_belongs_to_many :tags # use an implicit join table
end
class Author < ActiveRecord::Base
has_many :posts
end
class Tag < ActiveRecord::Base
has_and_belongs_to_many :posts # use an implicit join table
end
Is there any way I can filter all these associations to make sure they only return the posts that moderators have approved?
Yes. Without going into to much detail, the key technique is to define a scope on the Post model; something like this:
class Post < ActiveRecord::Base
scope :approved -> { where(status: Post.statuses['approved']) }
end
You can then show only the approved posts via Post.approved; or only the approved posts for a certain user via Post.where(user: foo).approved.
In order the display a list of tags, you could then, for example, just do:
Post.where(user: foo).approved.map(&:tags).uniq
This will only give you an Array, not an ActiveRecord::Collection. But that's probably good enough, for your purposes.
You can scope has_many itself.
Eg.
class Author
has_many :moderated_posts, -> { moderated }, class_name: 'Post'
end
class Post
scope :moderated, -> { where(status: approved_statuses) }
end

Avoid duplicate entry with has_many through rails

I have been using this nested attributes with has_many :through
class Check < ActiveRecord::Base
has_many :checks_tags, dependent: :destroy
has_many :tags, through: :checks_tags
attr_accessible :tags_attributes, :checks_tags_attributes
accepts_nested_attributes_for :tags, :checks_tags
end
class Tag < ActiveRecord::Base
has_many :checks_tags, dependent: :destroy
has_many :checks, through: :checks_tags
end
class CheckTag < ActiveRecord::Base
belongs_to :check
belongs_to :tag
end
so here the issue is when i create with this hash
"tags_attributes"=>[{"id"=>"", "name"=>"test12", "company_id"=>"1"}, {"id"=>"", "name"=>"test12", "company_id"=>"1"}]
actually here have two tags with same name, so its creating Tag twice and after putting twice on CheckTag, so is there any way to avoid this creation as twice in Tag?
If you want it forbidden in the database, you could create a unique index on the combination of the two columns on the check_tag table. If you want to handle it in rails, you can do it with a before_save callback on the Check model (if that's the only way you create these), but that may make you vulnerable to a race condition.
See this answer:
Index on multiple columns in RoR

Rails - get all possible values in a has_many through association

I have a very simple data model, as follows:
class Object < ActiveRecord::Base
has_many :object_tags
has_many :tags, through: :object_tags
end
class Tag < ActiveRecord::Base
has_many :object_tags
has_many :objects, through: :object_tags
end
class ObjectTag < ActiveRecord::Base
belongs_to :object
belongs_to :tag
end
Both Object and Tag have a name attribute.
What I want to do is, for a defined set of objects #objects, get an array of all the possible tags names that my set of objects can take. I would like to be able to do something like that (which is wrong, but just for illustration purpose) : #objects.pluck(:tags.name).uniq
I have tried quite a lot of things, with includes and joins, but nothing gets me to my result. The closest I got was #objects.includes(:tags).pluck(:tags), but it only gets my the array of tags, then I can't retrieve the name.
This might be a very straightforward question, but I am a beginner in Rails and could not find anything in my research.
Thanks for you help!
You can do it the following way:
#object.tags.uniq.pluck(:name)

Rails 4 create new Tag

I have a few questions about how to add a Tag to a users account:
Here is the User Model in relation to Tags
has_many :tags, through: :taggings
Here is the Tag Model:
class Tag < ActiveRecord::Base
attr_accessor :unread_count, :user_feeds
has_many :taggings
has_many :feeds, through: :taggings
end
And the Tagging Model:
class Tagging < ActiveRecord::Base
belongs_to :tag
belongs_to :feed
belongs_to :user
end
I'm at a point in the script where I have the current user object #user and I need to simply create a tag named "Mailbox" if it does not exist. I have tried a few create methods, and got expected object errors.
If anyone can help explain how to work with these models I would appreciate it.
A legit way to do this is to
#user.tags.create(name: "Mailbox")
If you want to check if it exists first, the rails 4 way is to:
#user.tags.find_or_create_by(name: "Mailbox")
Comment if you have any more question.
You can simply use the << method which the has_many association automatically adds:
#user.tags << Tag.find_or_create_by(name:'Mailbox')
Or, the rails 3 way:
#user.tags << Tag.where(name:'Mailbox').first_or_create!(name:'Mailbox')
# Or, as the << method automatically saves new objects
#user.tags << Tag.where(name:'Mailbox').first_or_initialize(name:'Mailbox')

Resources