Rails How to get all the grandchildren of an ojbect - ruby-on-rails

I have 3 models:
User
has_many :questions
has_many :corrections
end
Question
has_one :correction
belongs_to :user
end
Correction
belongs_to :user
belongs_to :question
So if user Bob asks a question then user Terry can check it and if its wrong offer a correction.
Lets stay with bob and assume he as kindly corrected 5 other users, i.e and lets assume he has been lucky to get 3 corrections from other users.
I want to be able to do something like this
#bob.corrections_offered => 5 correction objects
#bob.corrections_received => 3 correction objects
the first one is easy as its really just #bob.corrections under the hood. But I dont know how to implement the latter one. Can anyone help?
UPDATE
So I tried using through as suggested like so (Oh and actually the question model above is actually called Sentence in my code. I.e. User => Sentence => Correction. )
has_many :sentences
has_many :corrections_received, :through => :sentences, :class_name => 'Correction'
but got this error in console
ActiveRecord::HasManyThroughSourceAssociationNotFoundError:
Could not find the source
association(s) :Correction in model
Sentence. Try 'has_many
:corrections_received, :through =>
:sentences, :source => '. Is it
one of :language, :correction, :user,
or :checker?
So tried the following
has_many :corrections_received, :through => :sentences, :source => :correction
but got
ActiveRecord::HasManyThroughSourceAssociationMacroError:
Invalid source reflection macro
:has_one for has_many
:corrections_received, :through =>
:sentences. Use :source to specify
the source reflection.
not sure whats going wrong...

You can add a has_many through relationship in your user model like so
class User
#your usual relationships
has_many :corrections_received, :through => :questions, :class_name => 'Correction'
end

Try the following way:
has_many :corrections_received,:class_name=>'Correction',:conditions=>...

Normally, you should be able to do it with :through, but I'm not sure if two user->correction relationships are possible.
In any case, helper method in the model class should be simple enough. Something like this.
def corrections_received
result = Array.new
questions.each do |q|
if q.correction
result.push q.correction
end
end
result
end
Since I'm new to this stuff, corrections are welcome!

Related

Rails associations - ideas for best practice

A question regarding Rails associations.
Consider the following models:
People
Events
Images
People and events can have many images.
Images uploaded to an event, need to have the ability to be associated with multiple people.
This means that there are two relationships between people and images.
One where images are uploaded directly on the person. And one where a person is tagged in an event.
Can there also be a relationship between a person and an event based on the fact they were tagged in one (or multiple) images in an event? In this regard, it's a sort of image tagging system where associations are created based on what event people are tagged in.
Wondering what is the best practice in Rails to create this association? Any help or advice is greatly appreciated!
I am not absolutely sure about best practice,
but there is a chance to solve your case in simple way, as far as i understand you:
We have:
Event ... has_many :photos; Photo ... belongs_to :event
and solution is to create some intermediary ActiveRecord class
People ... has_many :photos, :through photo_mappings
PhotoMapping ... belongs_to :user; belongs_to :photo
Have a nice AR associations best practice1
In rails it is definitely possible to define a join-table that has extra fields. So in this case I would define the following table:
class LinkedImage
belongs_to :person
belongs_to :image
OWNER=1
TAGGED=2
validates :relation_type, :inclusion => {:in => [OWNER, TAGGED]}
end
This table would link an image to a person, and has an extra field relation_type (you could think of a more appropriate name maybe), which now can have two values: 1 (for the OWNER, meaning the image was directly uploaded to the person), and 2 (the person is tagged in the image). Aside from the relation, maybe you want to store something extra, like the position in the image, an extra comment, then you could easily add that here as well.
And the person would look like:
class Person
has_many :linked_images
has_many :uploaded_images, :class_name => Image,
:through => :linked_images,
:conditions => "relation_type=#{LinkedImage::OWNER}"
has_many :tagged_in_images, :class_name => Image,
:through => :linked_images,
:conditions => 'relation_type=#{LinkedImage::TAGGED}"
end
and the code for Image could be similar.
I hope this helps.
I don't know if this is the best approach, but I would do something like this:
class User < ActiveRecord::Base
has_many :images, :as => :owner
has_many :tags
has_many :tagged_images, :through => :tags, :source => :image
has_many :tagged_events, :finder_sql => %q( SELECT `events`.* FROM `events`, `images` WHERE `events`.id = `images`.owner_id AND `images.owner_type ="Event" `
WHERE (`images`.id IN (#{tagged_image_ids.join(',')}) ))
end
class Event < ActiveRecord::Base
has_many :images, :as => :owner
has_many :tagged_users, :finder_sql => %q( SELECT `users`.* FROM `users`, `images` WHERE `users`.id = `images`.owner_id AND `images.owner_type ="User" `
WHERE (`images`.id IN (#{image_ids.join(',')}) ))
end
class Tag < ActiveRecord::Base
belongs_to :user
belongs_to :image
end
class Image < ActiveRecord::Base
belongs_to :owner, :polymorphic => true
has_many :tags
has_many :tagged_users, :through => :tags, :source => :user
end
Note: Using finder_sql is not the best in Rails, because for example it doesn't allow you to add new tagged images just using the relationship, but it works fine for reading.

Multiple levels of have_many in a model

I am working with Ruby on Rails (specifically the ActiveRecord) and I am trying to decide whether or not it is a good idea to link my models using multiple levels.
class Student < ActiveRecord::Base
has_many :student_sections
has_many :sections, :through => :student_sections
has_many :courses, :through => :sections
end
It seems like this would work, but I don't have a lot of experience in ActiveRecord. Is there any reason not to do this?
This is fine but you should bear in mind that the courses association is effectively only a 'get' association (as opposed to 'get and set'). What i mean by that is that you can say
#student.courses
(after doing neo's fix) to get a list of courses, but you can't do
#student.courses << #course
as rails doesn't have the section info required to make the necessary joins between the student and the course.
you need to add :source attribute
has_many :sections, :through => :student_sections, :source => 'your_source'

Rails 3 associations

class User < ActiveRecord::Base
has_many :books
has_many :book_users
has_many :books, :through => :book_users
end
class Book < ActiveRecord::Base
belongs_to :user
has_many :book_users
has_many :users, :through => :book_users
end
An user can write many books
An book can belong to only one user
An user can be a reader of different books
An book can be read by different users
User.books
should give me the books the user has written
User.books_read
should give me the books, are read by this user
How accomplish this ?
Second question, what is the simplest method to delete book_read from the user ?
I mean
User.method_name(book_id) # what's the method name ?
First question:
You either use :source or :class_name.
has_many :books_read, :class_name => "Book", :through => :book_users
I don't know for sure if :class_name works with has_many :through. If it doesn't work, try this:
has_many :books_read, :source => :book, :through => :book_users
That should do the trick.
Second question:
As far as I know there isn't really a simple method to delete books from the books_read relation. You could create your own method to accomplish this. Just make sure you delete records from :book_users and not the has_many :through relation. Or else the books themselves will be deleted.
def delete_book(book_id)
self.book_users.find_by_book_id(book_id).destroy
end
When using Rails 3 you can't use the find_by_... helper and need to use where.
def delete_book(book_id)
self.book_users.where(:book_id => book_id).destroy
end
Now you can call this function as follows:
User.find(1).delete_book(2)
I hope that helps you out.

How to provide has_many through association through 3 models?

I hope someone has already experienced this.
Please help me, how can i solve this problem:
class Article < ActiveRecord::Base
belongs_to :author
belongs_to :publisher
has_one :address, :through => :publisher
end
class Author < ActiveRecord::Base
has_many :articles
has_many :addresses, :through => :articles, :source => :address
end
I try to get "addresses" for "author", and i get this error in console:
ActiveRecord::HasManyThroughSourceAssociationMacroError: Invalid source reflection macro :has_one :through for has_many :addresses, :through => :articles. Use :source to specify the source reflection.
but author.articles[0].address works fine.
I hope you give me advice, how can i solve it.
Thanks.
AR does not like sourcing a has_many through a has_one. But you can easily get all the addresses with this method on Author:
def addresses
articles.map {|article| article.address }
end
This solution also worked well for different relation types.
e.g. User.registrations.join_table.periods
but you -can't- apply active_record methods on what is mapped.
e.g. user.periods(:order => :date)
e.g. user.periods.model etc..
thanks

has_many :through issue with new records

I'm modeling "featuring" based on my plan in this question and have hit a bit of a stumbling block.
I'm defining a Song's primary and featured artists like this:
has_many :primary_artists, :through => :performances, :source => :artist,
:conditions => "performances.role = 'primary'"
has_many :featured_artists, :through => :performances, :source => :artist,
:conditions => "performances.role = 'featured'"
This works fine, except when I'm creating a new song, and I give it a primary_artist via new_song.performances.build(:artist => some_artist, :role => 'primary'), new_song.primary_artists doesn't work (since the performance I created isn't yet saved in the database).
What's the best approach here? I'm thinking of going with something like:
has_many :artists, :through => :performances
def primary_artists
performances.find_all{|p| p.role == 'primary'}.map(&:artist)
end
I think you're overcomplicating it. Just because things have similarities doesn't mean you should put them all in the same box.
class Song < ActiveRecord::Base
has_one :artist # This is your 'primary' artist
has_and_belongs_to_many :featured_artists, :source => :artist # And here you make a featured_artists_songs table for the simple HABTM join
validates_presence_of :artist
end
Poof, no more confusion. You still have to add song.artist before you can save, but that's what you wanted. Right?
There's not much that you can do about the association not being recognized until you save. Arguably, it doesn't really exist until you save, validations pass, and the relevant transaction(s) are completed.
Regarding your question of cleaning up your primary_artist method, you could model it something like this.
class Song < ActiveRecord::Base
has_many :performances
has_many :artists, :through => :performances
has_one :primary_artist, :through => :performances, :conditions => ["performances.roll = ?", "primary"], :source => :artist
end
It's unclear if you want one or many primary artists, but you can easily switch that has_one to has_many as needed.
You've nailed the source of your problem with build vs. create.
As for finding the primary artist of a song. I would add a named_scope on artist to select only featured/primary artists.
class Artist < ActiveRecord::Base
...
named\_scope :primary, :joins => :performances, :conditions => "performances.role = primary"
named\_scope :featured, :joins => :performances, :conditions => "performances.role = featured"
end
To get the primary artist for a song, you would do #song.artists.primary or if you prefer your primary_artists method in song.
def primary_artists
artists.primary
end
However, after looking at your initial question, I think your database layout is insufficient. It's workable but not clear, I've posted my suggestions there, where it belongs.
These named scopes I would also work under my proposed scheme as well.

Resources