one-to-many view help

OK... after reading the Agile Development book, the new E-Commerce book, parts of the Recipes book, numerous tutorials, screencasts and the like, I was certain that I was ready to begin....

I was wrong.

I have 2 models with a one-to-many relation.

class Photo < ActiveRecord::Base   belongs_to :listing # Every photo belongs to a listing end

class Listing < ActiveRecord::Base   has_many :photos # Every listing can have numerous photos end

so each listing can have numerous photos, and every photo belongs to a listing. Check.

I created a scaffold for the listing. Great, it shows all the listing data.

My problem is figuring out how to include the Photo data in with the listing data (a join, I think)

** Ultimately, when I look at the detail of a listing, I want to see the listing columns(name, year, etc), and all the photo columns for this listing(paths, image names). **

The generated controller and view code only handles the listing model, but I need it to include the photo model as well.

I know (roughly) how to use mysql commands to get the right columns, but I'm trying to let Rails do it's thing.

Being new, I'm still grasping for which methods are available to me to get the job done.

After programming in another language for a number of years, it's frustrating to know that you want to do something, but don't know enough about the language/framework to make it happen.

Thanks

Matt

Firstly, this is one of the reasons Rails developers grow out of scaffold very quickly. They are not association aware. You have described how the two models are related, which allows Rails to work its magic. Try placing:   <%= listing.photo.name %> (or similar depending on your schema) and see what happens.

You should see that Rails will goto the other table and pull back the data as if by magic. This is on of the great attractions (to me anyway) of rails - you rarely need to get your hand dirty with SQL.

If you want a list of available methods, then open a console (./scripts/console) and try soemthing like:   photos = Photo.find(:all)   photo.methods.sort

Since everything in Ruby is an object you can use this trick on anything - very handy!

Thanks for the console tips. Very handy indeed.

I still couldn't get it to work though....

[from show.rhtml]

# This is scaffold generated, but uses Listing class # and not listing variable (so I can't do listing.photo.name) # I see how I could use the Photo class, but of course that would # be purpose defeating :slight_smile:

  <% for column in Listing.content_columns %>    <p>      <b><%= column.human_name %>:</b> <%=h @listing.send(column.name) %>    </p>   <% end %>

# I was wondering if I need to do something different with my controller # to add the Photo column information into the @listing variable # as well ?? # from controller:

  def show     @listing = Listing.find(params[:id])   end

another thing that i noticed from the irb/console session, is that listing doesn't have a photo method listed. Is this expected? I was expecting that listing.photo.name indicated the listing object referencing a photo method, referencing a name . Am I wrong?

Thanks

Matt

  <% for column in Listing.content_columns %>    <p>      <b><%= column.human_name %>:</b> <%=h @listing.send(column.name) %>    </p>   <% end %>

Dump this scaffolding loop and write out the data yourself. Something like this

<%= @listing.name %><br /> <%= @listing.date %><br /> .... <% for photo in @listing.photo -%>   <%= photo.name %><br />   <%= photo.display %><br /> <% end -%>

  def show     @listing = Listing.find(params[:id])   end

This is faster: @listing = Listing.find(params[:id], :include => :photos)

But not necessary, especially for a show page. Rails automatically pulls in the photo objects when you do @listing.photos.

Try running "tail -f log/development.log" while browsing through your app and watch the sql that is being generated.

another thing that i noticed from the irb/console session, is that listing doesn't have a photo method listed. Is this expected? I was expecting that listing.photo.name indicated the listing object referencing a photo method, referencing a name . Am I wrong?

Theres is no "photo" method because a listing can have multiple photos. The method you want is listing.photos. If you don't see that then there is something wrong with your model relationships which doesn't appear to be the case. Make sure your looking at instance methods for not the class methods.

l = Listing.new l.methods.sort

NOT

Listing.methods.sort (unless you want to see class methods)

Aaron

Looks like the consensus is in.... don't use scaffold to write a real application, use it as a learning tool for the basics to figure out where stuff comes from and ends up. But the real application needs to be done by hand.

So any new guys/gals out there... that's your Holiday Tip!

Merry Christmas / Happy Hanukkah or just Happy Days to the rest !!

Matt

OK.. I got the show method working and went to try out the list method

This is what I have. Some is scaffold reuse and some new based on your mentor-ship on listing.photos.*

The listing.photos.send(column.name) line is my trouble spot, as column.name is finding the column from the Photo model, but claims an "undefined method `local_name' for Photo:Class" (local_name is a column in the DB) <% for listing in @listings %>   <tr>   <% for column in Listing.content_columns %>     <td><%=h listing.send(column.name) %></td>   <% end %>   <% for column in listing.photos.content_columns %>     <td><%=h listing.photos.send(column.name) %></td> #ERROR   <% end %>

I was thinking that column.name needs a prefix of some sort, but I'm clueless on what.

Matt

Matt,

listing.photos returns a collection (array) of photo objects. You need to loop on that array to print out info about each photo. Also, content_columns is a class method so you need to call it with the class not from an instance.

Photo.content_columns => returns columns p = Photo.new p.content_columns => error

Here is one way to loop on your photos collection <% for photo in listing.photos -%>   <% for column in Photo.content_columns -%>     <%= photo.send(column.name) -%>   <% end -%> <% end -%>

Aaron

Thanks. Now another question. I seem to be working backwards, working on the 'show' and 'listing' methods, before I have 'new' data.

I was looking at the SQL being generated from your examples, and they are doing exactly as I wanted, but no output. It was then that I realized I didn't have any proper data in them because my 'new' method and view aren't doing the right thing, and of course I don't see what I need to do.

So... This seems like such a simple task (one-to-many with CRUD operations, not drop down list, but join), that if you know of an example/tutorial site that has this, I would be greatful, otherwise, if you can answer the rest of my question, I'll put a tutorial together on this, so that you can point to it next time someone asks :slight_smile:

My scaffolded code (yes I know, we've been on this journey ), has the following :

[snip] <p><label for="listing_price">Price</label><br/> <%= text_field 'listing', 'price' %></p> [snip]

which is great for accessing the Listing model, but I need to access the Photo model to insert a photo_id into the Listing, and the photo_name into the Photo, and eventually I'll need to code up a way to get multiple photos into this listing, but if I can get just a single Photo into my listing, I think I can get it

if I do:

<p><label for="listing_price">Photo_Name</label><br/> <%= text_field 'photo', 'photo_name' %></p>

There is no link between Photo and Listing.

Like I said before, I know that this is going to be stupid easy, but only when the answer is known.

Thanks

Matt

my 'create' method has a listing = Listing.new(params[:listing])

is there a way to send in both :listing and :photos ?

I think that is all I'm missing.... this round.

Matt

Hello again Matt and Merry Christmas (if that's your thing).

The create method if fine - the params[:listing] looks at the hash (array) called "listing" to get the values, but your since the new listing does not yet exist (we have not created it just yet), we need to make it, then pull it's ID for the photo table.

Something like:

--ListingController   def create       listing = Listing.new(params[:listing]) # Create a new listing       photo = Photo.find(params[:photo_id]) # Find the photo using the ID passed to us       photo.listing_id = listing.id # Tie them together   end

Of course, this needs error checking, etc.

BTW - your current model only allows a photo to belong to one listing. If this is not what you want, have a look at "has_many :through".

Yes, Merry Christmas!

Do I need to have my view/form setting params for the :photo_id to be passed back to the create method ?

This line : photo = Photo.find(params[:photo_id])

Gives me this error: ** Couldn't find Photo without an ID **

I tried debugging at the console, but realized that I don't know how to emulate data coming back from a form.

I can post the development log if needed, but it is lengthy, so I didn't include

After being the top-dog in C++ for so many years, it's very humbling to struggle with a new language/framework.

Thanks for all your patience.

Matt

Yes, sorry; I assumed you were selecting the photo to add to the listing on the form and passing its ID.

Selecting, as in from a pre-defined dropdown? no. Both the RoR Ecommerce and Agile books both have examples of that.

I've get a Real Estate Listing admin page that needs to add an arbitrary number of photos to each listing (and then view, edit, delete).

The photos would be added at the same time as the listing, and not be a pre-defined "list" of photos to choose from.

I'm really having a hard time wrapping my head around this, and I can't find an example anywhere that is similar.

Sorry I haven't been as clear as I could with this. 4 Kids and Christmas, need I say more?

Matt

Ahh - that's a little different as you are creating the photos at the same time.

You will need a form that has a field to upload the pictures, ideally one that allows you to attach as many as you want.

The controller should then loop through the attached photos and assign them to the listing. Psuedo code:

--ListingController   def create       listing = Listing.new(params[:listing])       for params[:listing][:photos].each do |photo| # Assumes an array of photos has been passed in         temp = Photo.new(photo) # Needs to extract and save the binary data (file_column?)         temp.listing_id = listing.id # Tie them together         temp.save       end   end