Chapter 8. Improving Forms

Now that you can safely get information between your users and your applications, it’s time to examine some ways to do it better. Here are a few more features to explore:

  • Supporting file uploads, a common website feature that steps outside of the simple form field to database column mapping

  • Designing form builders, which make it easier to create forms that look the way you think they should, not the way Rails does it by default

Once you’ve figured out these pieces, you’ll have a reasonably complete understanding of the options Rails offers for creating classic web applications. Ajax still lies ahead, but the basics are still useful for a wide variety of situations.

Adding a Picture by Uploading a File

Since we’re building a collection of people, it might be nice to know what they look like. Adding file uploads to Rails applications requires making changes in several different places:

  • The form for creating and editing a person needs a file upload field.

  • The model representing person data needs to handle the file data.

  • A new migration needs to add a field for the file extension, because pictures come in different formats.

  • The view that shows a person should display the picture, too!

One key piece of a Rails application is missing here: the controller. The controller doesn’t actually need to do anything more than it is already doing: passing data between the view and the model. One more piece of data, even a big chunk like a photo file, isn’t going to make a difference to that default handling. (You can find the complete files for this example in ch08/guestbook007.)

Note

This chapter will show how to handle uploaded files directly. There are some plug-ins, notably attachment_fu, file_column, and paperclip, that can handle uploaded files for you. Unfortunately, they have their own challenges, particularly in installation, and won’t be covered in this book.

File Upload Forms

The simplest step seems to be adding the file upload field to the form:

<p>
  <b>Photo</b><br />
  <%= f.file_field :photo %>
</p>

Well, almost. Including a file upload changes the way an HTML form is submitted, changing it to a multipart form. For creating a new person, this means shifting from an HTML result that looks like:

<form action="/people" method="post">

to a result that looks like:

<form action="/people/" enctype="multipart/form-data" method="post">

Adding the enclosure type means that Rails will know to look for an attachment after the main body of form field data has arrived.

Addressing that means that there’s some additional work required on the form tag, created by the form_for method in our partial, _form.html.erb. In the old form, before the upload was added, it looked like:

<% form_for(@person) do |f| %>

In the new form, it has a lot more pieces:

<% form_for(:person, @person,
  :url => person_path(@person),
  :html => { :multipart => true,
     :method => (@person.new_record? ? :post : :put)}) do |f| %>

Why the sudden climb in complexity?

The shorter original version relies on Rails to do record identification, examining the object in @person and applying a whole set of defaults based on what it finds. When there’s a file upload involved, however, :multipart changes to true. Making that change means explicitly specifying a lot of the pieces Rails had taken care of for you previously, most notably the path for the URL and the method choice for submission. Rails does a lot of great things automatically, hiding complexity, but when its automatic choices aren’t the right ones, that complexity has to emerge.

The person_path(@person) call will check the routing tables to generate a URL, while @person.new_record? ? :post : :put checks to see if the @person object is empty. If it is, the form will use POST; if not, it will use PUT.

Note

Recent versions of Rails will let you avoid some of this complexity, shifting to the simpler form_for(@post, :html => {:multipart => true }). Still, it’s useful to understand what lurks underneath.

Model and Migration Changes

Adding a photo requires somewhat more effort than adding another ordinary field to the application, mostly because it (usually) doesn’t make sense to store potentially large chunks of media data like photos directly in a database. For this demonstration, it makes much better sense to store the photo in the filesystem, renamed to match the ID of the person it goes with.

There’s still one catch that requires accessing the database, though: photo files come in lots of different formats, and there’s little reason to restrict users to a single format. That will require keeping track of which file extension is used for the uploaded file by storing that in the database. Doing that will require creating a migration, in addition to adding a lot of logic to the model. The combination of filesystem and database use is shown in Figure 8-1.

Uploading a file into the public directory, with metadata stored in the database

Figure 8-1. Uploading a file into the public directory, with metadata stored in the database

A migration for an extension

Chapter 10 will explore migrations in much greater depth, but this migration is relatively simple. Rails will apply migrations in the sequence of their filenames, with the opening number being the critical piece. The db/migrate folder already contains a migration whose name ends in _create_people.rb, defining a CreatePeople class. To make it easy for us to figure out what’s going on, the next migration will contain an AddPhotoExtensionToPerson class. Following the same naming convention, this will be a timestamp followed by _add_photo_extension_to_person.rb. To create the migration file, enter:

script/generate migration add_photo_extension_to_person

Or, in Heroku, choose Generate from the gear menu and then enter migration add_photo_extension_to_person.

Note

For more detail on creating migrations by hand, see Chapter 10. This one is simple enough that you can probably follow along without the full tutorial, though.

The newly generated migration won’t have much in it, but you need to add details. There doesn’t need to be very much inside this migration, as it only creates (and destroys, if necessary) one field, :extension, of type :string:

class AddPhotoExtensionToPerson < ActiveRecord::Migration
  def self.up
    add_column :people, :extension, :string
  end

  def self.down
    remove_column :people, :extension
  end
end

When this migration is run, it will add a new column to the :people table that will be used to store :extension data. If rolled back, it deletes that column.

To run the migration, just run rake db:migrate as usual. The Rake tool will find the new migration file, know that it hasn’t run it yet, and add the column to the existing :people table, as requested.

Extending a model beyond the database

Data storage issues should all be handled in the model. Normally, Rails will save any properties that come into the model that have names corresponding to columns in the corresponding database table.

Note

Behind the scenes, ActiveRecord keeps track of which tables contain which columns and uses that information to generate a lot of code automatically. In development mode, it checks tables and generates code constantly, which is part of why development mode is slow but extremely flexible.

However, the migration just shown didn’t create a column that would map to :photo; just one for :extension. This is deliberate. Because these photos will be stored as files outside of the database, Rails shouldn’t handle them automatically. Explicit model code, in app/models/person.rb, will have to do that. Fortunately, Rails has an easy (and declarative) way to make sure the code for storing the photo runs after the rest of validation has happened, with its after_save callback method;

# after the person has been written to the database, deal with
# writing any image data to the filesystem
  after_save :store_photo

Unfortunately, Rails doesn’t have a built-in store_photo method. That requires coding.

Note

The after_save method is one of several callback methods supported by ActiveRecord. Note that there are after and before methods for create, destroy, save, update, validation, validation_on_create, and validation_on_update. If you need to tweak ActiveRecord’s data-handling, these can be valuable tools.

store_photo, the last method in the Person class, will call on some other methods that also need to be written, but it’s probably still easiest to look at store_photo first before examining the methods on which it depends:

private

# called after saving, to write the uploaded image to the filesystem
def store_photo
  if @file_data
    # make the photo_store directory if it doesn't exist already
    FileUtils.mkdir_p PHOTO_STORE
    # write out the image data to the file
    File.open(photo_filename, 'wb') do |f|
      f.write(@file_data.read)
    end
    # ensure file saved only when it newly arrives at model being saved
    @file_data = nil
  end
end

First, note that this method comes after the private keyword, making it invisible outside of the model class to which it belongs. Controllers and views shouldn’t be calling store_photo directly. Only other methods within the same model should be able to call it. (It’s not required that you make this method private, but it makes for cleaner code overall.)

Within the method itself, the first line, if @file_data, is simple—if there is actually data to be stored, then it’s worth proceeding. Otherwise, this isn’t necessary. Then there’s a call to Ruby’s file-handling classes, creating a directory for the photos. (This causes no harm if the directory already exists.) The next few lines open a file whose name is specified by photo_filename, write the data to it, and close it. At the end, store_photo sets @file_data to nil to make sure the file doesn’t get stored again elsewhere in the application.

This takes care of saving the file, which is the last thing done as the model finishes up its work on a form submission, but more details get attended to earlier, paving the way for saving the file. The photo= method takes care of a few details when a submission arrives:

# when photo data is assigned via the upload, store the file data
# for later and assign the file extension, e.g., ".jpg"
def photo=(file_data)
  unless file_data.blank?
    # store the uploaded data into a private instance variable
    @file_data = file_data
    # figure out the last part of the filename and use this as
    # the file extension. e.g., from "me.jpg" will return "jpg"
    self.extension = file_data.original_filename.split('.').last.downcase
  end
end

The def for this method looks a bit unusual because it takes advantage of a Rails convention. Writing def photo=(file_data) creates a method that grabs the file_data content for :photo, which Rails creates based on the contents of the file_field from the HTML form. It defines what happens when person.photo is assigned a value. That file_data content gets moved to an @file_data instance variable that is private to the model but is accessible to any of the methods within it. (@file_data is what store_photo referenced, for instance.)

The photo= method also handles the one piece of the filename that will get stored in the database—the file extension. It gets the original name, splits off the piece after the last ., and lowercases it. (You don’t have to be this draconian, but it does make for simpler maintenance.) Note that photo= just assigns a value to the extension variable of the current Person object. ActiveRecord will save that value automatically, as it maps to the :extension column created by the migration.

The next few pieces are filename housekeeping:

# File.join is a cross-platform way of joining directories;
# we could have written "#{RAILS_ROOT}/public/photo_store"
PHOTO_STORE = File.join RAILS_ROOT, 'public', 'photo_store'

# where to write the image file to
  def photo_filename
    File.join PHOTO_STORE, "#{id}.#{extension}"
  end

# return a path we can use in HTML for the image
  def photo_path
    "/photo_store/#{id}.#{extension}"
  end

PHOTO_STORE provides the application with a path to this Rails application’s public directory, where static files can go. The photo_filename method gets called by store_photo when it needs to know where the photo file should actually go on its host machine’s filesystem. You can see that instead of preserving the original filename, it uses the id—the primary key number for this Person—when it creates a name for the photo. This may seem like overkill, but it has the convenient virtue of avoiding filename conflicts. Otherwise, if multiple people had uploaded me.jpg, some of them would be surprised by the results.

The photo_path method handles filename housekeeping for views that need to display the image. It’s unconcerned with where the file exists in the server’s file system and focuses instead on where it will appear as a URL in the Rails application. Again, photo_path uses the id to create the name. Its one line, a string, actually is the return value.

There’s another housekeeping function that supports the view. Not everyone will necessarily have a photo, and broken image icons aren’t particularly attractive. To simplify dealing with this, the model includes a has_photo method that checks to see if there’s a file corresponding to the id and extension of the current record:

# if a photo file exists, then we have a photo
  def has_photo?
    File.exists? photo_filename
  end

Remember, Ruby will treat the last value created by a method as its return value—in this case, the response to File.exists?. This returns true if there is a file corresponding to the id and extension, and false if there isn’t.

Showing it off

The last piece that the application needs is a way to show off the picture. That’s a simple addition in the show.html.erb view:

<p>
  <b>Photo:</b>
    <% if @person.has_photo? %>
      <%= image_tag @person.photo_path %>
    <% else %>
      No photo.
    <% end %>
</p>

The has_photo? method from the model lets the view code decide whether or not to create an img element for the photo. If there is one, it uses the model’s photo_path method for the src attribute, pointing to the file in the public directory’s photo_store directory. If not, there’s plain text with the message “No photo” rather than a broken image icon.

Results

It’s time to try this code. Running rake db:migrate updates the database:

== 2 AddPhotoExtensionToPerson: migrating ========================
-- add_column(:people, :extension, :string)
   -> 0.0757s
== 2 AddPhotoExtensionToPerson: migrated (0.0759s) ===============

Running ruby script/server fires up the application, which at first glance looks very similar to earlier versions, as shown in Figure 8-2. (And yes, displaying everyone’s “secret” isn’t very secret, but we’ll get to a much better solution in Chapter 12.)

A list of users who might have photos

Figure 8-2. A list of users who might have photos

If you click the “New Person” link or go to edit an existing record, you’ll see a new field for the photo, highlighted in Figure 8-3.

When a photo is uploaded, it is stored in the application’s public directory, in a photo_store directory, as shown in Figure 8-4. Note that there is a skipped number—only records which actually have photos leave any trace here.

A file field in the person form

Figure 8-3. A file field in the person form

Stored photos in the public/photo_store directory

Figure 8-4. Stored photos in the public/photo_store directory

Showing a page for a record that includes a photo yields the photo embedded in the page, as shown in Figure 8-5. (Note that at present there aren’t any constraints on photo size. You could constrain it, but you’ll have to install a graphics library, configure it, and connect it to Ruby and Rails.)

A record displaying an uploaded photo

Figure 8-5. A record displaying an uploaded photo

Records that don’t have an associated photo just get the “No photo” message shown in Figure 8-6.

A record unadorned with a photo—but spared a broken image icon

Figure 8-6. A record unadorned with a photo—but spared a broken image icon

This isn’t quite a simple process, but multimedia usually stretches frameworks built around databases. Rails is flexible enough to let you choose how to handle incoming information and work with the file system to develop a solution that fits. (And if this isn’t quite the right solution for you, don’t worry—many people are working out their own solutions to these issues and sharing them.)

Note

It is possible for programs treating your application as a REST-based web service to send photos as multipart/form-data. However, Rails’ default approach to generating XML responses won’t give them an easy way to retrieve the photos unless the programs understand the photo_store/id.extension convention.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset