has_many autosave_associated_records_for_* - ruby-on-rails

I am trying to associate existing records while still being able to add new ones. The following does not work but is pretty close to what I need. How can I accomplish associating existing records and creating new ones?
has_many :comments, :through => :commentings, :source => :commentable, :source_type => "Comment"
accepts_nested_attributes_for :comments, :allow_destroy => true
def autosave_associated_records_for_comments
comments.each do |comment|
if existing_comment = Comment.find_by_fax_and_name(comment.fax, comment.name)
self.comments.reject! { |hl| hl.fax == existing_comment.fax && hl.name == existing_comment.name }
self.comments << existing_comment
else
self.comments << comment
end
end
end
Here is a relevant line of source: https://github.com/rails/rails/blob/v3.0.11/activerecord/lib/active_record/autosave_association.rb#L155

I've made a solution, but if you know of a better way to do this please let me know!
def autosave_associated_records_for_comments
existing_comments = []
new_comments = []
comments.each do |comment|
if existing_comment = Comment.find_by_fax_and_name(comment.fax, comment.name)
existing_comments << existing_comment
else
new_comments << comment
end
end
self.comments << new_comments + existing_comments
end

I have a tagging system that utilizes a has_many :through relationship. Neither of the solutions here got me where I needed to go so I came up with a solution that may help others. This has been tested on Rails 3.2.
Setup
Here are a basic version of my Models:
Location Object:
class Location < ActiveRecord::Base
has_many :city_taggables, :as => :city_taggable, :dependent => :destroy
has_many :city_tags, :through => :city_taggables
accepts_nested_attributes_for :city_tags, :reject_if => :all_blank, allow_destroy: true
end
Tag Objects
class CityTaggable < ActiveRecord::Base
belongs_to :city_tag
belongs_to :city_taggable, :polymorphic => true
end
class CityTag < ActiveRecord::Base
has_many :city_taggables, :dependent => :destroy
has_many :ads, :through => :city_taggables
end
Solution
I did indeed override the autosave_associated_record_for method as follows:
class Location < ActiveRecord::Base
private
def autosave_associated_records_for_city_tags
tags =[]
#For Each Tag
city_tags.each do |tag|
#Destroy Tag if set to _destroy
if tag._destroy
#remove tag from object don't destroy the tag
self.city_tags.delete(tag)
next
end
#Check if the tag we are saving is new (no ID passed)
if tag.new_record?
#Find existing tag or use new tag if not found
tag = CityTag.find_by_label(tag.label) || StateTag.create(label: tag.label)
else
#If tag being saved has an ID then it exists we want to see if the label has changed
#We find the record and compare explicitly, this saves us when we are removing tags.
existing = CityTag.find_by_id(tag.id)
if existing
#Tag labels are different so we want to find or create a new tag (rather than updating the exiting tag label)
if tag.label != existing.label
self.city_tags.delete(tag)
tag = CityTag.find_by_label(tag.label) || CityTag.create(label: tag.label)
end
else
#Looks like we are removing the tag and need to delete it from this object
self.city_tags.delete(tag)
next
end
end
tags << tag
end
#Iterate through tags and add to my Location unless they are already associated.
tags.each do |tag|
unless tag.in? self.city_tags
self.city_tags << tag
end
end
end
The above implementation saves, deletes and changes tags the way I needed when using fields_for in a nested form. I'm open to feedback if there are ways to simplify. It is important to point out that I am explicitly changing tags when the label changes rather than updating the tag label.

In Rails 6 (and probably earlier versions too), it's easy to overwrite the attributes writer generated by accepts_nested_attributes_for and use find_or_initialize_by.
In this case, you could simply write:
has_many :comments, :through => :commentings, :source => :commentable, :source_type => "Comment"
accepts_nested_attributes_for :comments, :allow_destroy => true
def comments_attributes=(hashes)
hashes.each { |attributes| comments << Comment.find_or_initialize_by(attributes)
end
Haven't tested this when passing :id or :_destroy keys, as it doesn't apply in my situation, but feel free to share your thoughts or code if you do.
I would love to see Rails implement this natively, perhaps by passing an upsert: true option to accepts_nested_attributes_for.

Related

Check if paperclip attachment has changed

So in order to reduce duplication, I broke image attachments away from a Product model onto an Image model, so that similar products could share an image. Like so
class Product < ActiveRecord::Base
has_many :imaged_items
has_many :images, :through => :imaged_items
end
class ImagedItems < ActiveRecord::Base
belongs_to :product
belongs_to :image
end
class Image < ActiveRecord::Base
has_many :imaged_items
has_many :products, :through => :imaged_items
end
What I would like to do is if I'm editing a product, I'd like to check whether the image has changed before I save it. If the image has changed, I don't want to update the existing record, I want to create a new one since the old image could be used by multiple Products.
Is there any way to do that? I'm stumped.
GO Through Class: ActiveRecord::Dirty ...You can use changed
person.changed # => []
person.name = 'bob'
person.changed # => ['name']
####OR ..other way as well
person.name = 'Bill'
person.name_changed? # => false
person.name_change # => nil
I had the problem in an after_update callback and I use .dirty? instead of thumb_changed?
after_update :foo, if: Proc.new{
|model| model.title_changed? || model.summary_changed? || model.thumb.dirty?
}
Worked for me perfectly. Hope it helps

Multiple entries in a :has_many through association

I need some help with a rails development that I'm working on, using rails 3.
This app was given to me a few months ago just after it's inception and I have since become rather fond of Ruby.
I have a set of Projects that can have resources assigned through a teams table.
A team record has a start date and a end date(i.e. when a resource was assigned and de-assigned from the project).
If a user has been assigned and deassigned from a project and at a later date they are to be assigned back onto the project,
instead of over writting the end date, I want to create a new entry in the Teams table, to be able to keep a track of the dates that a resource was assigned to a certain project.
So my question is, is it possible to have multiple entries in a :has_many through association?
Here's my associations:
class Resource < ActiveRecord::Base
has_many :teams
has_many :projects, :through => :teams
end
class Project < ActiveRecord::Base
has_many :teams
has_many :resources, :through => :teams
end
class Team < ActiveRecord::Base
belongs_to :project
belongs_to :resource
end
I also have the following function in Project.rb:
after_save :update_team_and_job
private
def update_team_and_job
# self.member_ids is the selected resource ids for a project
if self.member_ids.blank?
self.teams.each do |team|
unless team.deassociated
team.deassociated = Week.current.id + 1
team.save
end
end
else
self.teams.each do |team|
#assigning/re-assigning a resource
if self.member_ids.include?(team.resource_id.to_s)
if team.deassociated != nil
team.deassociated = nil
team.save
end
else
#de-assigning a resource
if team.deassociated == nil
team.deassociated = Week.current.id + 1
team.save
end
end
end
y = self.member_ids - self.resource_ids
self.resource_ids = self.resource_ids.concat(y)
self.member_ids = nil
end
end
end
Sure, you can have multiple associations. has_many takes a :uniq option, which you can set to false, and as the documentation notes, it is particularly useful for :through rel'ns.
Your code is finding an existing team and setting deassociated though, rather than adding a new Team (which would be better named TeamMembership I think)
I think you want to just do something like this:
add an assoc for active memberships (but in this one use uniq: => true:
has_many :teams
has_many :resources, :through => :teams, :uniq => false
has_many :active_resources,
:through => :teams,
:class_name => 'Resource',
:conditions => {:deassociated => nil},
:uniq => true
when adding, add to the active_resources if it doesn't exist, and "deassociate" any teams that have been removed:
member_ids.each do |id|
resource = Resource.find(id) #you'll probably want to optimize with an include or pre-fetch
active_resources << resource # let :uniq => true handle uniquing for us
end
teams.each do |team|
team.deassociate! unless member_ids.include?(team.resource.id) # encapsulate whatever the deassociate logic is into a method
end
much less code, and much more idiomatic. Also the code now more explicitly reflects the business modelling
caveat: i did not write a test app for this, code may be missing a detail or two

Rails ActiveRecord updating fields

My Example:
class Category < ActiveRecord::Base
has_many :tags, :as => :tagable, :dependent => :destroy
def tag_string
str = ''
tags.each_with_index do |t, i|
str+= i > 0 ? ', ' : ''
str+= t.tag
end
str
end
def tag_string=(str)
tags.delete_all
Tag.parse_string(str).each { |t| tags.build(:tag => t.strip) }
end
end
How would you optimize this tag_string field? I don't want to delete all the tags every time I would like to just update them. Is there a better way to parse string with tags?
I do not want use a plugin! Thx.
I know you don't want to use a plugin, but you might want to dig through the source of acts_as_taggable_on_steroids to see how they're handling these situations. From my experience, working with that plugin has been very painless.
class Category < ActiveRecord::Base
has_many :tags, :as => :tagable, :dependent => :destroy
def tag_string
tags.map {|t| t.name }.join ', '
end
def tag_string=(str)
tags = Tag.parse_string(str)
end
end
I don't know what Tag.parse_string(str) method do. If it returns an array of Tag objects, than my example should work. And I'm not sure if this will only update, or delete old and add new one. You can test it and look in logs what it really does.
I agree with other commenters. You are better off using the plugin here. Here is one solution.
class Category < ActiveRecord::Base
has_many :tags, :as => :tagable, :dependent => :destroy
def tag_string
tags.collect(&:name).join(", ")
end
def tag_string=(str)
# Next line will delete the old association and create
# new(based on the passed str).
# If the Category is new, then save it after the call.
tags = Tag.create(str.split(",").collect{ |name| {:name => name.strip} })
end
end

Rails model with foreign_key and link table

I am trying to create a model for a ruby on rails project that builds relationships between different words. Think of it as a dictionary where the "Links" between two words shows that they can be used synonymously. My DB looks something like this:
Words
----
id
Links
-----
id
word1_id
word2_id
How do I create a relationship between two words, using the link-table. I've tried to create the model but was not sure how to get the link-table into play:
class Word < ActiveRecord::Base
has_many :synonyms, :class_name => 'Word', :foreign_key => 'word1_id'
end
In general, if your association has suffixes such as 1 and 2, it's not set up properly. Try this for the Word model:
class Word < ActiveRecord::Base
has_many :links, :dependent => :destroy
has_many :synonyms, :through => :links
end
Link model:
class Link < ActiveRecord::Base
belongs_to :word
belongs_to :synonym, :class_name => 'Word'
# Creates the complementary link automatically - this means all synonymous
# relationships are represented in #word.synonyms
def after_save_on_create
if find_complement.nil?
Link.new(:word => synonym, :synonym => word).save
end
end
# Deletes the complementary link automatically.
def after_destroy
if complement = find_complement
complement.destroy
end
end
protected
def find_complement
Link.find(:first, :conditions =>
["word_id = ? and synonym_id = ?", synonym.id, word.id])
end
end
Tables:
Words
----
id
Links
-----
id
word_id
synonym_id
Hmm, this is a tricky one. That is because synonyms can be from either the word1 id or the word2 id or both.
Anyway, when using a Model for the link table, you must use the :through option on the Models that use the Link Table
class Word < ActiveRecord::Base
has_many :links1, :class_name => 'Link', :foreign_key => 'word1_id'
has_many :synonyms1, :through => :links1, :source => :word
has_many :links2, :class_name => 'Link', :foreign_key => 'word2_id'
has_many :synonyms2, :through => :links2, :source => :word
end
That should do it, but now you must check two places to get all the synonyms. I would add a method that joined these, inside class Word.
def synonyms
return synonyms1 || synonyms2
end
||ing the results together will join the arrays and eliminate duplicates between them.
*This code is untested.
Word model:
class Word < ActiveRecord::Base
has_many :links, :dependent => :destroy
has_many :synonyms, :through => :links
def link_to(word)
synonyms << word
word.synonyms << self
end
end
Setting :dependent => :destroy on the has_many :links will remove all the links associated with that word before destroying the word record.
Link Model:
class Link < ActiveRecord::Base
belongs_to :word
belongs_to :synonym, :class_name => "Word"
end
Assuming you're using the latest Rails, you won't have to specify the foreign key for the belongs_to :synonym. If I recall correctly, this was introduced as a standard in Rails 2.
Word table:
name
Link table:
word_id
synonym_id
To link an existing word as a synonym to another word:
word = Word.find_by_name("feline")
word.link_to(Word.find_by_name("cat"))
To create a new word as a synonym to another word:
word = Word.find_by_name("canine")
word.link_to(Word.create(:name => "dog"))
I'd view it from a different angle; since all the words are synonymous, you shouldn't promote any one of them to be the "best". Try something like this:
class Concept < ActiveRecord::Base
has_many :words
end
class Word < ActiveRecord::Base
belongs_to :concept
validates_presence_of :text
validates_uniqueness_of :text, :scope => :concept_id
# A sophisticated association would be better than this.
def synonyms
concept.words - [self]
end
end
Now you can do
word = Word.find_by_text("epiphany")
word.synonyms
Trying to implement Sarah's solution I came across 2 issues:
Firstly, the solution doesn't work when wanting to assign synonyms by doing
word.synonyms << s1 or word.synonyms = [s1,s2]
Also deleting synonyms indirectly doesn't work properly. This is because Rails doesn't trigger the after_save_on_create and after_destroy callbacks when it automatically creates or deletes the Link records. At least not in Rails 2.3.5 where I tried it on.
This can be fixed by using :after_add and :after_remove callbacks in the Word model:
has_many :synonyms, :through => :links,
:after_add => :after_add_synonym,
:after_remove => :after_remove_synonym
Where the callbacks are Sarah's methods, slightly adjusted:
def after_add_synonym synonym
if find_synonym_complement(synonym).nil?
Link.new(:word => synonym, :synonym => self).save
end
end
def after_remove_synonym synonym
if complement = find_synonym_complement(synonym)
complement.destroy
end
end
protected
def find_synonym_complement synonym
Link.find(:first, :conditions => ["word_id = ? and synonym_id = ?", synonym.id, self.id])
end
The second issue of Sarah's solution is that synonyms that other words already have when linked together with a new word are not added to the new word and vice versa.
Here is a small modification that fixes this problem and ensures that all synonyms of a group are always linked to all other synonyms in that group:
def after_add_synonym synonym
for other_synonym in self.synonyms
synonym.synonyms << other_synonym if other_synonym != synonym and !synonym.synonyms.include?(other_synonym)
end
if find_synonym_complement(synonym).nil?
Link.new(:word => synonym, :synonym => self).save
end
end

how to access rails join model attributes when using has_many :through

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

Resources