Adding existing records to has_many :through relationship - ruby-on-rails

I'm building a dictionary application where I have some concepts linked to some words using a join model. I have a form to edit each concept, where I can edit its words, add new words to it or associate existing words with it.
I'm using accepts_nested_attributes_for and update_attributes, but this only works with the already associated words or the newly created ones. For the existing words that weren't previously associated, I get errors like this ActiveRecord::RecordNotFound (Couldn't find Word with ID=4 for Concept with ID=1000) when ActiveRecord runs a select on the join model.
Here are my models:
class Word < ActiveRecord::Base
attr_accessible :notes, :text, :grammar_tag_list
has_many :semantic_relations, :dependent => :destroy
has_many :concepts, :through => :semantic_relations
end
class Concept < ActiveRecord::Base
attr_accessible :meaning_tag_list, :words_attributes
has_many :semantic_relations, :dependent => :destroy
has_many :words, :through=> :semantic_relations, :order => 'knowledge_id ASC'
accepts_nested_attributes_for :words, :reject_if => lambda { |w| w[:text].blank? }
end
class SemanticRelation < ActiveRecord::Base
belongs_to :word, :inverse_of => :semantic_relations
belongs_to :concept, :inverse_of => :semantic_relations
end
I have managed it to work using the following method in my Concepts controller (calling it before update_attributes), but it seems a quite dirty approach. Isn't there any proper way to achieve this?
def check_words
current_ids = #concept.word_ids
new_ids = params[:concept][:words_attributes].map do |w|
id = w[1][:id].to_i
unless id == 0
unless current_ids.include? id
id
end
end
end
#concept.word_ids = current_ids + new_ids unless new_ids.blank?
end

Related

Relation Conditions with Variables

I have the following models:
class Publication < ActiveRecord::Base
has_many :reviews
has_many :users, :through => :owned_publications
has_many :owned_publications
end
class User < ActiveRecord::Base
has_many :publications, :through => :owned_publications
has_many :owned_publications
end
class OwnedPublication < ActiveRecord::Base
belongs_to :publication
belongs_to :user
has_one :review, :conditions => "user_id = #{self.user.id} AND publication_id = #{self.publication.id}"
end
In the third model, I'm trying to set a condition with a pair of variables. It seems like the syntax works, except that self is not an instance of OwnedPublication. Is it possible to get the current instance of OwnedPublication and place it into a condition?
The solution requires the use of :through and :source options, as well as a proc call:
has_one :review, :through => :publication, :source => :reviews,
:conditions => proc { ["user_id = ?", self.user_id] }
Proc is the trick to passing in dynamic variables to ActiveRecord association conditions, at least as of Rails 3.0. Simply calling:
has_one :conditions => proc { ["publication_id = ? AND user_id = ?",
self.publication_id, self.user_id] }
will not work, though. This is because the association will end up searching the reviews table for a 'reviews.owned_publication_id' column, which does not exist. Instead, you can find the proper review through publication, using publication's :reviews association as the source.
I think your best bet is to just have the Review record belong_to an OwnedPublication, and setup your Publication model to get the reviews via a method:
def reviews
review_objects = []
owned_publications.each do |op|
review_objects << op
end
review_objects
end
Might be a more efficient way if you use a subquery to get the information, but it removes the concept of having unnecessary associations.

Rails 3.1 - Inheriting values from a join table?

I have the following models: Releases, Tracks & a has_many join called ReleasesTrack.
I also have Products that (semi) successfully inherit a releases tracks with Track & Release Ids being copied to a ProductsTrack has_many_through join.
The problem is i'm not getting the correct position value.
I currently have this in ProductsTrack model, it appears to work, but i'm not getting the value I want.
before_save do
self.position = self.track.position
end
Instead of the position value in the track table, I want the position from it's has_many_through join table releases_tracks. I've tried the following a variations thereof, but no joy:
before_save do
self.position = self.track.releases_track.position
end
I did think having a position field in both Tracks & ReleasesTracks could be causing the issue and there is a reason I have this in both, but I've tested with a temp field and it's not that.
I think the crux of the issue is structring self.track.releases_track.position correctly.
OR
I'm missing something in an association?
Any ideas?
EDIT: MODELS ADDED (Note, ProductsTrack is actually the badly named Producttracklisting)
class Release < ActiveRecord::Base
has_many :products, :dependent => :destroy
has_many :releases_tracks, :dependent => :destroy, :after_add => :position_track
has_many :tracks, :through => :releases_tracks, :order => "releases_tracks.position"
accepts_nested_attributes_for :tracks, :reject_if => lambda { |a| a[:name].blank? }, :allow_destroy => :true
accepts_nested_attributes_for :releases_tracks
def position_track(track)
releases_tracks.each { |t| t.position = t.track.position }
end
def track_attributes=(track_attributes)
track_attributes.each do |attributes|
tracks.build(attributes)
artists_tracks.build(attributes)
end
end
end
class Track < ActiveRecord::Base
has_many :releases_tracks, :dependent => :destroy
has_many :releases, :through => :releases_tracks
has_many :producttracklistings, :dependent => :destroy
has_many :products, :through => :producttracklistings
end
class ReleasesTrack < ActiveRecord::Base
belongs_to :release
belongs_to :track
end
class Producttracklisting < ActiveRecord::Base
belongs_to :product
belongs_to :track
before_save do
self.position = self.track.position
end
end
class Product < ActiveRecord::Base
belongs_to :release
has_many :releases_tracks, :through => :release, :source => :tracks
has_many :producttracklistings, :dependent => :destroy
has_many :tracks, :through => :producttracklistings
accepts_nested_attributes_for :tracks, :reject_if => lambda { |a| a[:name].blank? }, :allow_destroy => :true
accepts_nested_attributes_for :producttracklistings
#Below is where a product inherits tracks from the parent release
before_save do
self.track_ids = self.releases_track_ids
end
end
> I have the following models; Releases, Tracks & a has_many_through join called ReleasesTrack
So you have
class Release < ActiveRecord::Base
has_many :release_tracks
has_many :tracks, :through => :release_tracks
end
class Track < ActiveRecord::Base
has_many :release_tracks
has_many :releases, :through => :release_tracks
end
class ReleaseTrack < ActiveRecord::Base
belongs_to :release
belongs_to :track
end
> I also have Products that (semi) successfully inherit a releases tracks with Track & Release Ids being copied to a ProductsTrack has_many_through join.
class Products < ReleaseTrack
#uses table release_tracks
#belongs_to :release #from inheritance
#belongs_to :track #from inheritance
end
> I currently have this in ProductsTrack model, it appears to work, but i'm not getting the value I want.
What ProductsTrack model? Did you mean before that you have a ProductsTrack that inherits from a release tracks?
class ProductsTrack < ReleaseTrack
#uses table release_tracks
#belongs_to :release #from inheritance
#belongs_to :track #from inheritance
before_save do
self.position = self.track.position
end
end
> Instead of the position value in the track table, I want the position from it's has_many_through join table releases_tracks.
So far, the way I've coded it base on what you said, self.position is the position from the has_many_through join table release_tracks.
Your data model probably needs to be cleaned up. It's probably way more complicated than you need, but, given the model, let's sort things out.
There is no inheritance. All the record models inherit from ActiveRecord::Base.
You basically have releases, tracks, and products with a many-to-many relationship between releases and tracks, and another many-to-many relationship between tracks and products.
Also, products belongs_to releases and products has_many :releases_tracks through it's release.
Your question is
> I currently have this in ProductsTrack model, it appears to work, but i'm not getting the value I want.
> Instead of the position value in the track table, I want the position from it's has_many_through join table releases_tracks.
You do not list a ProductsTrack model, so I assume this is the Producttracklisting class. You want to set the position field in the producttracklistings table to "the position from it's has_many_through join table releases_tracks".
So the answer is that you cannot do this, because there is not one release_tracks record for the given producttracklistings record. The producttracklistings points to a (belongs_to) a tracks record, but the releases_tracks records point to the tracks record (not the other way around), so there can be any number of release_tracks records from 0 to many, which your question refers to.

using has_many :through and build

i have three models, all for a has_many :through relationship. They look like this:
class Company < ActiveRecord::Base
has_many :company_users, dependent: :destroy
has_many :users, through: :company_users
accepts_nested_attributes_for :company_users, :users
end
class CompanyUser < ActiveRecord::Base
self.table_name = :companies_users #this is because this was originally a habtm relationship
belongs_to :company
belongs_to :user
end
class User < ActiveRecord::Base
# this is a devise model, if that matters
has_many :company_users, dependent: :destroy
has_many :companies, through: :company_users
accepts_nested_attributes_for :company_users, :companies
end
this loads fine, and the joins are built fine for queries. However, whenever i do something like
#company = Company.last
#user = #company.users.build(params[:user])
#user.save #=> true
#company.save #=> true
both the User record and the CompanyUser records get created, but the company_id field in the CompanyUser record is set to NULL
INSERT INTO `companies_users` (`company_id`, `created_at`,`updated_at`, `user_id`)
VALUES (NULL, '2012-02-19 02:09:04', '2012-02-19 02:09:04', 18)
it does the same thing when you #company.users << #user
I'm sure that I'm doing something stupid here, I just don't know what.
You can't use a has_many :through like that, you have to do it like this:
#company = Company.last
#user = User.create( params[:user] )
#company.company_users.create( :user_id => #user.id )
Then you will have the association defined correctly.
update
In the case of the comment below, as you already have accepts_nested_attributes_for, your parameters would have to come like this:
{ :company =>
{ :company_users_attributes =>
[
{ :company_id => 1, :user_id => 1 } ,
{ :company_id => 1, :user_id => 2 },
{ :company_id => 1, :user_id => 3 }
]
}
}
And you would have users being added to companies automatically for you.
If you have a has_many :through association and you want to save an association using build you can accomplish this using the :inverse_of option on the belongs_to association in the Join Model
Here's a modified example from the rails docs where tags has a has_many :through association with posts and the developer is attempting to save tags through the join model (PostTag) using the build method:
#post = Post.first
#tag = #post.tags.build name: "ruby"
#tag.save
The common expectation is that the last line should save the "through" record in the join table (post_tags). However, this will not work by default. This will only work if the :inverse_of is set:
class PostTag < ActiveRecord::Base
belongs_to :post
belongs_to :tag, inverse_of: :post_tags # add inverse_of option
end
class Post < ActiveRecord::Base
has_many :post_tags
has_many :tags, through: :post_tags
end
class Tag < ActiveRecord::Base
has_many :post_tags
has_many :posts, through: :post_tags
end
So for the question above, setting the :inverse_of option on the belongs_to :user association in the Join Model (CompanyUser) like this:
class CompanyUser < ActiveRecord::Base
belongs_to :company
belongs_to :user, inverse_of: :company_users
end
will result in the following code correctly creating a record in the join table (company_users)
company = Company.first
company.users.build(name: "James")
company.save
Source: here & here
I suspect your params[:user] parameter, otherwise your code seems clean. We can use build method with 1..n and n..n associations too, see here.
I suggest you to first make sure that your model associations works fine, for that open the console and try the following,
> company = Company.last
=> #<Tcompany id: 1....>
> company.users
=> []
> company.users.build(:name => "Jake")
=> > #<User id: nil, name: "Jake">
> company.save
=> true
Now if the records are being saved fine, debug the parameters you pass to build method.
Happy debugging :)

Joining friends to other models

I'm still having trouble building complex joins in ActiveRecord.
I have a User model that is using the HasManyFriends plugin by Steve Ehrenberg (http://dnite.org).
Then I have a UserFeedEvent model that links users to a FeedEvent model.
What I'd like to achieve is to find all the FeedEvents linked to the friends of a User.
How should I write my ActiveRecord query?
Here are my models:
class User < ActiveRecord::Base
has_many_friends
has_many :feed_events, :through => :user_feed_events, :dependent => :destroy
has_many :user_feed_events, :dependent => :destroy
end
class UserFeedEvent < ActiveRecord::Base
belongs_to :feed_event, :dependent => :destroy
belongs_to :user
end
class FeedEvent < ActiveRecord::Base
has_many :user_feed_events, :dependent => :destroy
has_many :users, :through => :user_feed_events
serialize :data
end
Thanks in advance!
Augusto
Digging through HasManyFriends source leads me to believe that the following should work (or be half-way through):
EDIT: found out that source cannot point to another :has_many :through association. So you could try the updated version.
class User < ActiveRecord::Base
#...
has_many :user_feed_events_of_friends_by_me, :through => :friends_by_me,
:source => :user_feed_events
has_many :feed_events_of_friends_by_me, :through => :user_feed_events_by_me
has_many :user_feed_events_of_friends_for_me, :through => :friends_for_me,
:source => :user_feed_events
has_many :feed_events_of_friends_for_me, :through => :user_feed_events_for_me
# A wrapper to return full list of two-way friendship events
def feeds_events_of_my_friends
self.feed_events_of_friends_by_me + self.feed_events_of_friends_for_me
end
end
Unfortunately the HMF plugin has two one-way friendship links, which means full list requires 2 DB queries.
I found a working and more traditional SQL solution:
friends_id = current_user.friends.collect {|f| f.id}.join(",")
sql = "SELECT feed_events.*, user_feed_events.user_id FROM feed_events LEFT JOIN user_feed_events ON feed_events.id = user_feed_events.feed_event_id WHERE user_feed_events.user_id IN (#{friends_id}) GROUP BY feed_events.id ORDER BY feed_events.created_at DESC"
friend_feed_events = FeedEvent.paginate_by_sql(sql, :page => params[:page], :per_page => 30)
If you have a more efficient / more elegant way of doing the same, please let me know!

How can I use form_for to update an association's has_many :through association

In my form for member_profile, I would like to have role checkboxes that are visible for admins. I would like to used some nested form_for, but can't make it work, so I've resorted to manually creating the check_box_tags (see below), and then manually adding them to member_profile.member.
Note that the Member model is Devise, and I don't want to mix those fields in with my MemberProfile data, in case I change auth systems in the future.
class Member < ActiveRecord::Base
has_one :member_profile
has_many :member_roles
has_many :roles, :through => :member_roles
end
class MemberProfile < ActiveRecord::Base
belongs_to :member
has_many :member_roles, :through => :member
#has_many :roles, :through => :member_roles #can't make this work work
end
class Role < ActiveRecord::Base
has_many :member_roles
validates_presence_of :name
end
class MemberRole < ActiveRecord::Base
belongs_to :member
belongs_to :role
end
Form (haml)
= form_section do
- Role.all.each do |x|
=check_box_tag 'member[role_ids][]',
x.id,
begin #resource.member.role_ids.include?(x.id) rescue nil end
=x.name
member_profiles_controller.rb
def update
if #resource.update_attributes params[:member_profile]
#resource.member.role_ids = params[:member][:role_ids]
redirect_to(#resource, :notice => 'Member profile was successfully updated.')
else
render :action => "edit"
end
end
I've decided it only makes sense to do a nested has_many :through on Update, since the join model is what is being 'gone through' to get to the has_many :through model. Before the hmt is created, there is obviously no record in the join model.

Resources