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.
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.)
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.
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.
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.
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
.
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.upadd_column :people, :extension, :string
end def self.downremove_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.
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.
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.
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_photoif @file_data
# make the photo_store directory if it doesn't exist alreadyFileUtils.mkdir_p PHOTO_STORE
# write out the image data to the fileFile.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_filenameFile.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.
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.
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.)
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.
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.)
Records that don’t have an associated photo just get the “No photo” message shown in Figure 8-6.
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.)
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.