How To Get Additional Attributes From Has Many Through - ruby-on-rails

I am using Rails 3 beta 4.
I have the following models:
class Player < ActiveRecord::Base
has_many :players_items, :dependent => :destroy
has_many :items, :through => :players_items
end
class PlayersItem < ActiveRecord::Base
belongs_to :player
belongs_to :item
end
class Item < ActiveRecord::Base
has_many :players_items, :dependent => :destroy
has_many :players, :through => :players_items
end
In the players_controller
def items
#player = Player.find(params[:id])
#player_items = #player.items
end
I have the following attributes
--Items Model--
Item_id:Integer
Name:String
Cost:Integer
Description:Text
--PlayersItem Model--
Item_id:Integer
Player_id:Integer
Total:Integer
Traded:Integer
I am trying to print out all the items associated with a player and for each item print out the "Name", "Cost", "Description", "Total", and "Traded" values.
When I call #player_items in the items.html.erb, I can only access the attributes associated with the Item Model and not any of the attributes associated with PlayersItem model.
I am trying to access the attributes from both the items model and players_items model in the same "call" similar to SQL Join Statement like this
SELECT * FROM players_items INNER JOIN items ON players_items.item_id=items.id
WHERE players_items.player_id = "#player"
Is this possible?

#player = Player.order("created_at").last
#player.players_items.each do |item|
puts "#{item.player}: #{item.description} cost:#{item.cost}"
end
Has many through is a little weird. Think of it as a model whose name should (ideally) be descriptive of the relationship between the two other models. So maybe if equipment is being distributed to the players you could call the join model distributions or loans or something. The players_items is the naming convention for join tables which aren't going to be addressed directly.
I hope that helps!

Used in the controller
#player = Player.find(params[:id], :include => [:items,:players_items])
And in the view
#player.players_items.each do |player|
puts "#{player.name}: #{player.player.item.description} cost:#{player.item.cost}"
end

Related

Rails create object has_many through

My models
class Collection < ActiveRecord::Base
has_many :outfits
has_many :products, through: :outfits
end
class Outfit < ActiveRecord::Base
belongs_to :product
belongs_to :collection
end
class Product < ActiveRecord::Base
has_many :outfits
has_many :collections, through: :outfits
end
I want to save product in collection model
so one collection can have few product in it
How can i do it? i'm a bit struggle with it
it have tried something like this
p = Product.find_by_code('0339').id
p.collections.create(product_id:p1)
but i guess i'm wrong
When you're chaining through a through collection you don't need to reference the parent's id since that is known.
Instead of:
p = Product.find_by_code('0339').id # NOTE that you have an 'id' not an object here
p.collections.create(product_id:p1) # you can't call an association on the id
Build the association between two existing models (I'm assuming you have other fields in your models; I'm using name as an example).
p = Product.find_by(code: '0339')
c = Collection.find_by(name: 'Spring 2016 Clothing')
o = Outfit.new(name: 'Spring 2016 Outfit', product: p, collection: c)
o.save!
Assuming p and c exist and assuming o passes validation then you now have an assocaition between one product and one collection using a new outfit as the join table.
p.reload
p.collections.count # => 1
c.reload
c.products.count # => 1

Creating a Object.New with a foreign key results in nil?

I have two models:
class Todo < ActiveRecord::Base
belongs_to :assigned_to, :class_name => "TeamMembership"
end
and
class TeamMembership < ActiveRecord::Base
has_many :assigned_todos, :class_name => 'Todo', :foreign_key => :assigned_to, :dependent => :destroy
end
when I do:
Todo.new(:title=>'hello',:assigned_to=>TeamMembership.first)
(where TeamMembership.first is a valid record from the database), I get a Todo with
:title = hello
:assigned_to = nil
Why is :assigned_to nil, and how can I fix it?
Thanks!
The "Rails way" to do this would be:
class Todo < ActiveRecord::Base
belongs_to :team_membership
end
class TeamMembership < ActiveRecord::Base
has_many :todos, depdendent: :destroy
end
And a t.references :team_membership line in the create_table block for your migration that creates the todos table.
Then to create a new Todo with the title "hello" that belongs to the first TeamMembership, you'd do:
TeamMembership.first.todos.build(title: "hello")
See http://guides.rubyonrails.org/association_basics.html#has-many-association-reference
There may be some specific reason that you want to rename the association & foreign key, but you can still use the build method on the collection of todos.
have you added any validation? if yes then try on console Todo.new(:title=>'hello',:assigned_to=>TeamMembership.first,:validate=>'false')
and see the result other wise please tell me the atributes of Todo
OK, so it turns out the problem was:
The database fields need to be renamed with _id as a suffix, creator_id etc.
Then you need to put the ID in as an integer, rather than pass in the object.

Rails has_many through avoiding duplication

I have a has_many through association setup between a song model and an artist model.
My code looks something like this
SongArtistMap Model
class SongArtistMap < ActiveRecord::Base
belongs_to :song
belongs_to :artist
end
Artist Model
class Artist < ActiveRecord::Base
has_many :song_artist_maps
has_many :songs, :through => :song_artist_maps
validates_presence_of :name
end
Song Model
class Song < ActiveRecord::Base
has_many :song_artist_maps
has_many :artists, :through => :song_artist_maps
accepts_nested_attributes_for :artists
end
I have a form where a user submits a song and enters in the song title and the song artist.
So when a user submits a song and my Artists table doesn't already have the artist for the song I want it to create that artist and setup the map in SongArtistMap
If a user submits a song with an artist that is already in the Artists table I just want the SongArtistMap created but the artist not duplicated.
Currently everytime a user submits a song a new artist gets created in my artists table even if the same one already exists and a SongArtistMap is created for that duplicated artist.
Any idea on how to tackle this issue? I feel like rails probably has some easy little trick to fix this already built in. Thanks!
Ok I got this figured out awhile ago and forgot to post. So here's how I fixed my problem. First of all I realized I didn't need to have a has_many through relationship.
What I really needed was a has_and_belongs_to_many relationship. I setup that up and made the table for it.
Then in my Artists model I added this
def self.find_or_create_by_name(name)
k = self.find_by_name(name)
if k.nil?
k = self.new(:name => name)
end
return k
end
And in my Song model I added this
before_save :get_artists
def get_artists
self.artists.map! do |artist|
Artist.find_or_create_by_name(artist.name)
end
end
And that did exactly what I wanted.
I use a method in the model of the table the other two go through, that is called with before_create. This can probably be made much neater and faster though.
before_create :ensure_only_one_instance_of_a_user_in_a_group
private
def ensure_only_one_instance_of_a_user_in_a_group
user = User.find_by_id(self.user_id)
unless user.groups.empty?
user.groups.each do |g|
if g.id == self.group_id
return false
end
end
end
return true
end
Try this:
class Song < ActiveRecord::Base
has_many :song_artist_maps
has_many :artists, :through => :song_artist_maps
accepts_nested_attributes_for :artists, :reject_if => :normalize_artist
def normalize_artist(artist)
return true if artist['name'].blank?
artist['id'] = Artist.find_or_create_by_name(artist['name']).id
false # This is needed
end
end
We are essentially tricking rails by over-loading the reject_if function(as we never return true).
You can further optimize this by doing case insensitive lookup ( not required if you are on MySQL)
artist['id'] = (
Artist.where("LOWER(name) = ? ", artist['name'].downcase).first ||
Artist.create(:name => artist['name'])
).id

Rails - Join Table with Attributes (HABTM + Through) But how do I create / delete records?

I have a User model and Interest Model being joined by a join table called Choice (details below). I'm using the HABTM relationship with through since I have an attribute within the join table as well.
User.rb
has_many :choices
has_many :interests, :through => :choices
Interest.rb
has_many :choices
has_many :users, :through => :choices
Choice.rb
belongs_to :user
belongs_to :interest
So the question is how do I add records to this newly created choice table. For example =>
#user = User.find(1)
#interest = Interest.find(1)
????? Choice << user.id + interest.id + 4(score attribute) ??????
The last part is the part I'm having a problem with..I have these 3 parameters and didn't how to add them in and what the syntax was?
You have a couple options for adding a choice, but what probably makes the most sense would be to add choices by scoping to the user instance:
Assuming:
#user = User.find(1)
#interest = Interest.find(1)
You could add a choice like so:
#user.choices.create(:interest => #interest, :score => 4)
You could also do something like this in your controller:
def create
#choice = #user.choices.build(params[:choice])
if #choice.save
# saved
else
# not saved
end
end
This assumes your form has fields for choice[:interest_id] and choice[:score]

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