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