How to have an ordered model association allowing duplicates - ruby

I have two models, Song and Show. A Show is an ordered list of Songs, in which the same Song can be listed multiple times.
That is, there should be an ordered array (or hash or anything) somewhere in Show that can contain Song1, Song2, Song1, Song3 and allow re-ordering, inserting, or deleting from that array.
I cannot figure out how to model this with ActiveRecord associations. I'm guessing I need some sort of special join table with a column for the index, but apart from starting to code my SQL directly, is there a way to do this with Rails associations?
Some code as I have it now (but doesn't work properly):
class Song < ActiveRecord::Base
attr_accessible :title
has_and_belongs_to_many :shows
end
class Show < ActiveRecord::Base
attr_accessible :date
has_and_belongs_to_many :songs
end
song1 = Song.create(title: 'Foo')
song2 = Song.create(title: 'Bar')
show1 = Show.create(date: 'Tomorrow')
show1.songs << song1 << song2 << song1
puts "show1 size = #{show1.songs.size}" # 3
show1.delete_at(0) # Should delete the first instance of song1, but leave the second instance
puts "show1 size = #{show1.songs.size}" # 2
show1.reload
puts "show1 size = #{show1.songs.size}" # 3 again, annoyingly
Inserting might look like:
show1.songs # Foo, Bar, Foo
song3 = Song.create(title: 'Baz')
show1.insert(1, song3)
show1.songs # Foo, Baz, Bar, Foo
And reordering might (with a little magic) look something like:
show1.songs # Foo, Bar, Foo
show1.move_song_from(0, to: 1)
show1.songs # Bar, Foo, Foo

You're on the right track with the join table idea:
class Song < ActiveRecord::Base
attr_accessible :title
has_many :playlist_items
has_many :shows, :through => :playlist_items
end
class PlaylistItem < ActiveRecord::Base
belongs_to :shows #foreign_key show_id
belongs_to :songs #foreign_key song_id
end
class Show < ActiveRecord::Base
attr_accessible :date
has_many :playlist_items
has_many :songs, :through => :playlist_items
end
Then you can do stuff like user.playlist_items.create :song => Song.last

My current solution to this is a combination of has_many :through and acts_as_list. It was not the easiest thing to find information on combining the two correctly. One of the hurdles, for example, was that acts_as_list uses an index starting at 1, while the array-like methods created by the ActiveRecord association start at 0.
Here's how my code ended up. Note that I had to specify explicit methods to modify the join table (for most of them anyway); I'm not sure if there's a cleaner way to make those work.
class Song < ActiveRecord::Base
attr_accessible :title
has_many :playlist_items, :order => :position
has_many :shows, :through => :playlist_items
end
class PlaylistItem < ActiveRecord::Base
attr_accessible :position, :show_id, :song_id
belongs_to :shows
belongs_to :songs
acts_as_list :scope => :show
end
class Show < ActiveRecord::Base
attr_accessible :date
has_many :playlist_items, :order => :position
has_many :songs, :through => :playlist_items, :order => :position
def song_at(index)
self.songs.find_by_id(self.playlist_items[index].song_id)
end
def move_song(index, options={})
raise "A :to option is required." unless options.has_key? :to
self.playlist_items[index].insert_at(options[:to] + 1) # Compensate for acts_as_list starting at 1
end
def add_song(location)
self.songs << location
end
def remove_song_at(index)
self.playlist_items.delete(self.playlist_items[index])
end
end
I added a 'position' column to my 'playlist_items' table, as per the instructions that came with acts_as_list. It's worth noting that I had to dig into the API for acts_as_list to find the insert_at method.

Related

Rail ActiveRecord get all many to many relations

I have looked through similar questions on here but cant seem to make sense of them.
I have 3 tables Items, Sizes, and Item_Sizes where Items has many Sizes through Item_Sizes.
item.rb:
class Item < ApplicationRecord
has_many :images
has_many :category_items
has_many :categories, through: :category_items
has_many :item_sizes
has_many :sizes, through: :item_sizes
has_many :colour_items
has_many :colours, through: :colour_items
end
size.rb
class Size < ApplicationRecord
has_many :item_sizes
has_many :items, :through => :item_sizes
end
item_size.rb:
class ItemSize < ApplicationRecord
belongs_to :item
belongs_to :size
end
That all seems to be working. But I'm wondering how to get all Sizes that are associated with a subset of items.
I have written a loop to do this but I doubt it's very efficient.
def get_all_sizes(items)
results = []
items.each do |item|
item.sizes.each do |size|
results << size unless results.include?(size)
end
end
results
end
Is there a proper query I could use in place of this for loop?
I think you could join the Item_Sizes table and get the sizes by the items' ids:
Size.joins(:item_sizes).where(item_sizes: { item_id: items.ids })
You can do it in one line.
result = items.map{|item| item.sizes }.flatten.uniq

ActiveRecord get all + associated details

I am trying to retrieve a list of all tasks, where each task has a developer and reviewer. I am able to retrieve the list but it contains developer_id and reviewer_id. How do I retrieve a list containing developer name and retriever name?
class Person < ActiveRecord::Base
end
class Unread_Object < ActiveRecord::Base
belongs_to :person
end
class Developer < Person
has_many :tasks
end
class Reviewer < Person
has_many :tasks
has_many :unread_objects
end
class Task < ActiveRecord::Base
belongs_to :developer
belongs_to :reviewer
has_many :documents
after_save :add_task_to_unread_objects
protected
def add_task_to_unread_objects
Person.find_each do |person|
Unread_Object.create(
:person_id => person.id,
:internal_object_id => self.internal_object_id,
:unread_cause => 'Create')
end
end
end
Things I have tried.
get '/taskslist' do
#Task.includes([:developer, :reviewer]).all.to_json
#Task.joins(:developer,:reviewer).select("tasks.*, people.*").to_json #works somewhat but only shows one name
#Task.includes(:reviewer.name,:developer.name).all.to_json #"undefined method `name' for :reviewer:Symbol"
#Task.find(:all, :include => {:people => :name}).to_json #Couldn't find all Tasks with 'id': (all, {:include=>{:people=>:name}})
end
I hope to get Tasks json with nested json for developer, reviewer and other objects.
This question is follow up of this.
After some searching found as_json(include: <association>)
So this works
Task.includes(:developer,:reviewer).all.as_json(include: [:developer,:reviewer]).to_json
But other alternatives need to be seen.

find_or_create_by on intermediate table in rails 2.3

I have 2 tables , related to a third table as follows.
class A < ActiveRecord::Base
has_many :B, :through => :AB
has_many :AB
end
class B < ActiveRecord::Base
has_many :A, :through => :AB
has_many :AB
end
class AB < ActiveRecord::Base
belongs_to :B,
belongs_to :A
end
I want to be able to insert into the intermediate table(AB), using find_or_create.But find_or_create doesnot work on AB. How can I do it, other than finding and creating seperately ??
PS: I dont want to use has_and_belongs_to_many because that doesnot create the intermediate model and i want to insert into the intermediate table without creating any extra rows in the end tables, A and B.
Some suggestions please??
find_or_create_by has been added in rails 4. You can write it by yourself (should go to initializers):
class ActiveRecord::Base
def self.find_by(params)
scoped.where(params).limit(1).first
end
def self.find_or_create_by(params)
find_by(params) || create(params)
end
end

ActiveRecord association seems to read differently depending on reading forward or backward

I've read a ton of ActiveRecord SO Questions and haven't come across this yet. I know that the following code snippet is a little long winded but I'm not using Rails so I wanted to be clear about how my database was created/structured.
A household has the head (or heads) of household (like mom and dad), and it has children. Mom, Dad and the kids are all members of the household. I tried to implement that as follows. This is the full code snippet so you can just copy and paste and run it if you have active_record and sqlite3.
I wrote the question in-line in the code, but here it is in case you don't want to skim the code: when I do household.heads I get the members which I assigned as heads of household. But when I run member.household.heads (on the same household!) I don't get the heads, I get the kids! My only thought is that I shouldn't be using two 'has_many's with the same foreign_key, but everything else I've tried doesn't work.
require 'active_record'
ActiveRecord::Base.logger = Logger.new(File.open('database.log', 'w'))
ActiveRecord::Base.establish_connection(
:adapter => 'sqlite3',
:database => 'test.db'
)
ActiveRecord::Schema.define do
unless ActiveRecord::Base.connection.tables.include? 'members'
create_table :members do |table|
table.integer :household_id
table.integer :head_id
table.integer :child_id
table.string :name
end
end
unless ActiveRecord::Base.connection.tables.include? 'households'
create_table :households do |table|
table.string :address
end
end
end
class Member < ActiveRecord::Base
belongs_to :household
end
class Household < ActiveRecord::Base
has_many :heads, class_name: "Member", foreign_key: :household_id
has_many :children, class_name: "Member", foreign_key: :household_id
end
#Create some members
m1 = Member.create(name: "Foo")
m2 = Member.create(name: "Bar") #Foo's wife
m3 = Member.create(name: "foo-foo") #Foo and Bar's little girl
#Create a household
h1 = Household.create(address: "123 Fake St.")
#Assign members to households
h1.heads = [m1, m2]
h1.children = [m3]
#first let's check, h1 is m1's household. The two are the same.
p h1.id == m1.household.id
#So why doesn't this work?
h1.heads.each{|head| p head.name} #returns Foo and Bar
m1.household.heads.each{|head| p head.name} #<= Doesn't return Foo and Bar ?!?
h1.children.each{|child| p child.name} #returns foo-foo
m1.household.children.each{|child| p child.name} #Also returns foo-foo as expected
As it turns out, ActiveRecord is not OK with two Models of the same class being referred to by the same foreign key (household_id). You're just overwriting the assignment of h1's members when you assign children (or something like that, this still isn't clear to me).
You either need different classes: Head and Child or you need to be ok with the child calling the household differently.
I ended up changing the foreign keys to: household_id and house_id since I need heads.household more than I need child.household
(I still think it is annoying to use children.house but oh well!)
To be clear:
class Household < ActiveRecord::Base
has_many :heads, class_name: "Member", foreign_key: :household_id
has_many :children, class_name: "Member", foreign_key: :house_id
end

Polymorphic has_many self-referential

I have A number of models (Article, Video, Photo)
Now I am trying to create a related_to association, such that
An article can have many other articles, videos and photos related to it. As can videos and photos.
Heres what I have tried:
module ActsAsRelatable
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def acts_as_relatable
has_many :related_items, :as => :related
has_many :source_items, :as => :source, :class_name => 'RelatedItem'
end
end
end
class RelatedItem < ActiveRecord::Base
belongs_to :source, :polymorphic => true
belongs_to :related, :polymorphic => true
end
Then I have added acts_as_relatable to my three models (Article, Video, Photo) and included the module in ActiveRecord::Base
When trying in ./script/console I get it to add the related items and the ids work correctly however the source_type and related_type are always the same (the object that related_items was called from) I want the related_item to be the other model name.
Any ideas anyone?
I would use the has many polymorphs plugin since it supports double sided polymorphism you can do something like this:
class Relating < ActiveRecord::Base
belongs_to :owner, :polymorphic => true
belongs_to :relative, :polymorphic => true
acts_as_double_polymorphic_join(
:owners => [:articles, :videos, :photos],
:relatives => [:articles, :videos, :photos]
)
end
and don't forget the db migration:
class CreateRelatings < ActiveRecord::Migration
def self.up
create_table :relating do |t|
t.references :owner, :polymorphic => true
t.references :relative, :polymorphic => true
end
end
def self.down
drop_table :relatings
end
end
I don't know if "Relating" is a good name, but you get the idea. Now an article, video and photo can be related to another article, video or photo.

Resources