Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Agile Web Development With Rails, 2nd Edition (2006).pdf
Скачиваний:
30
Добавлен:
17.08.2013
Размер:
6.23 Mб
Скачать

WORKING WITH NONMODEL FIELDS 498

Forms Containing Collections

If you need to edit multiple objects from the same model on one form, add open and closed brackets to the name of the instance variable you pass to the form helpers. This tells Rails to include the object’s id as part of the field name. For example, the following template lets a user alter one or more image URLs associated with a list of products.

Download e1/views/app/views/array/edit.rhtml

<% form_tag do %>

<% for @product in @products %>

<%= text_field("product[]" , 'image_url') %><br /> <% end %>

<%= submit_tag %> <% end %>

When the form is submitted to the controller, params[:product] will be a hash of hashes, where each key is the id of a model object and the corresponding value are the values from the form for that object. In the controller, this could be used to update all product rows with something like

Download e1/views/app/controllers/array_controller.rb

Product.update(params[:product].keys, params[:product].values)

your user interactions. They will also help when you need to follow accessibility guidelines for your applications. I recommend using form builders for all your Rails forms.

22.7Working with Nonmodel Fields

So far we’ve focused on the integration between models, controllers, and views in Rails. But Rails also provides support for creating fields that have no corresponding model. These helper methods, documented in FormTagHelper, all take a simple field name, rather than a model object and attribute. The contents of the field will be stored under that name in the params hash when the form is submitted to the controller. These nonmodel helper methods all have names ending in _tag.

We need to create a form in which to use these field helpers. So far we’ve been using form_for to do this, but this assumes we’re building a form around a model object, and this isn’t necessarily the case when using the low-level helpers.

We could just hard-code a <form> tag into our HTML, but Rails has a better way: create a form using the form_tag helper. Like form_for, a form_tag should

Report erratum

WORKING WITH NONMODEL FIELDS 499

appear within <%...%> sequences and should take a block containing the form contents.8

<% form_tag :action => 'save', :id => @product do %> Quantity: <%= text_field_tag :quantity, '0' %>

<% end %>

The first parameter to form_tag is a hash identifying the action to be invoked when the form is submitted. This hash takes the same options as url_for (see page 404). An optional second parameter is another hash, letting you set attributes on the HTML form tag itself. (Note that the parameter list to a Ruby method must be in parentheses if it contains two literal hashes.)

<% form_tag({ :action => :save }, { :class => "compact" }) do ...%>

We can illustrate nonmodel forms with a simple calculator. It prompts us for two numbers, lets us select an operator, and displays the result.

The file calculate.rhtml in app/views/test uses text_field_tag to display the two number fields and select_tag to display the list of operators. Note how we had to initialize a default value for all three fields using the values currently in the params hash. We also need to display a list of any errors found while processing the form data in the controller and show the result of the calculation.

Download e1/views/app/views/test/calculate.rhtml

<% unless @errors.blank? %>

<ul>

<% for error in @errors %>

<li><p><%= h(error) %></p></li>

<% end %>

</ul>

<% end %>

<% form_tag(:action => :calculate) do %>

<%= text_field_tag(:arg1, @params[:arg1], :size => 3) %> <%= select_tag(:operator,

options_for_select(%w{ + - * / }, @params[:operator])) %>

<%= text_field_tag(:arg2, @params[:arg2], :size => 3) %> <% end %>

<strong><%= @result %></strong>

8. This is a change in Rails 1.2.

Report erratum

WORKING WITH NONMODEL FIELDS 500

Without error checking, the controller code would be trivial.

def calculate

if request.post?

@result = Float(params[:arg1]).send(params[:op], params[:arg2]) end

end

However, running a web page without error checking is a luxury we can’t afford, so we’ll have to go with the longer version.

Download e1/views/app/controllers/test_controller.rb

def calculate

if request.post? @errors = []

arg1 = convert_float(:arg1) arg2 = convert_float(:arg2)

op = convert_operator(:operator)

if @errors.empty? begin

@result = op.call(arg1, arg2) rescue Exception => err

@result = err.message end

end end

end

private

def convert_float(name) if params[name].blank?

@errors << "#{name} missing" else

begin

Float(params[name]) rescue Exception => err

@errors << "#{name}: #{err.message}" nil

end end

end

def convert_operator(name) case params[name]

when "+" then proc {|a,b| a+b} when "-" then proc {|a,b| a-b} when "*" then proc {|a,b| a*b} when "/" then proc {|a,b| a/b} else

@errors << "Missing or invalid operator" nil

end end

Report erratum

UPLOADING FILES TO RAILS APPLICATIONS 501

It’s interesting to note that most of this code would evaporate if we were using Rails model objects, where much of this housekeeping is built in.

Old-Style form_tag

Prior to Rails 1.2, form_tag did not take a block. Instead, it generated the <form> element as a string. You call it using something like

<%= form_tag :action => :save %>

... form contents ...

<%= end_form_tag %>

You can still use form_tag this way in Rails 1.2, but this use is disapproved of unless you have a compelling need to avoid the block form. (And it’s hard to come up with a real-world need that can’t be handled by the block form— perhaps a template when the form starts in one file and ends in another?)

To drive home the fact that this use of form_tag is frowned upon, Rails has deprecated the end_form_tag helper: you’ll now have to resort to

<%= form_tag :action => :save %>

... form contents ...

</form>

The ugliness of this is supposed to make you stop and think....

22.8Uploading Files to Rails Applications

Your application may allow users to upload files. For example, a bug-reporting system might let users attach log files and code samples to a problem ticket, or a blogging application could let its users upload a small image to appear next to their articles.

In HTTP, files are uploaded as a multipart/form-data POST message. As the name suggests, this type of message is generated by a form. Within that form, you’ll use one or more <input> tags with type="file". When rendered by a browser, this tag allows the user to select a file by name. When the form is subsequently submitted, the file or files will be sent back along with the rest of the form data.

To illustrate the file upload process, we’ll show some code that allows a user to upload an image and display that image alongside a comment. To do this, we first need a pictures table to store the data.

Download e1/views/db/migrate/003_create_pictures.rb

class CreatePictures < ActiveRecord::Migration def self.up

create_table :pictures do |t|

t.column

:comment,

:string

t.column

:name,

:string

Report erratum

UPLOADING FILES TO RAILS APPLICATIONS 502

t.column :content_type, :string

#If using MySQL, blobs default to 64k, so we have to give

#an explicit size to extend them

t.column :data,

:binary, :limit => 1.megabyte

end

 

end

 

def self.down drop_table :pictures

end end

We’ll create a somewhat artificial upload controller just to demonstrate the process. The get action is pretty conventional; it simply creates a new picture object and renders a form.

Download e1/views/app/controllers/upload_controller.rb

class UploadController < ApplicationController def get

@picture = Picture.new end

# . . .

end

The get template contains the form that uploads the picture (along with a comment). Note how we override the encoding type to allow data to be sent back with the response.

Download e1/views/app/views/upload/get.rhtml

<%= error_messages_for("picture") %>

<% form_for(:picture,

:url => {:action => 'save'},

:html => { :multipart => true }) do |form| %>

Comment:

<%=

form.text_field("comment") %><br/>

Upload your picture: <%=

form.file_field("uploaded_picture") %><br/>

<%= submit_tag("Upload file") %> <% end %>

The form has one other subtlety. The picture is uploaded into an attribute called uploaded_picture. However, the database table doesn’t contain a column of that name. That means that there must be some magic happening in the model.

Download e1/views/app/models/picture.rb

class Picture < ActiveRecord::Base

validates_format_of :content_type, :with => /^image/,

:message => "-- you can only upload pictures"

Report erratum

UPLOADING FILES TO RAILS APPLICATIONS 503

def uploaded_picture=(picture_field)

self.name

= base_part_of(picture_field.original_filename)

self.content_type = picture_field.content_type.chomp

self.data

= picture_field.read

end

def base_part_of(file_name) File.basename(file_name).gsub(/[^\w._-]/, '')

end end

We define an accessor called uploaded_picture= to receive the file uploaded by the form. The object returned by the form is an interesting hybrid. It is filelike, so we can read its contents with the read method; that’s how we get the image data into the data column. It also has the attributes content_type and original_filename, which let us get at the uploaded file’s metadata. All this picking apart is performed by our accessor method: a single object is stored as separate attributes in the database.

Note that we also add a simple validation to check that the content type is of the form image/xxx. We don’t want someone uploading JavaScript.

The save action in the controller is totally conventional.

Download e1/views/app/controllers/upload_controller.rb

def save

@picture = Picture.new(params[:picture]) if @picture.save

redirect_to(:action => 'show', :id => @picture.id) else

render(:action => :get) end

end

So, now that we have an image in the database, how do we display it? One way is to give it its own URL and simply link to that URL from an image tag. For example, we could use a URL such as upload/picture/123 to return the image for picture 123. This would use send_data to return the image to the browser. Note how we set the content type and filename—this lets browsers interpret the data and supplies a default name should the user choose to save the image.

Download e1/views/app/controllers/upload_controller.rb

def picture

@picture = Picture.find(params[:id]) send_data(@picture.data,

:filename => @picture.name, :type => @picture.content_type, :disposition => "inline")

end

Report erratum

UPLOADING FILES TO RAILS APPLICATIONS 504

Figure 22.3: Uploading a File

Finally, we can implement the show action, which displays the comment and the image. The action simply loads up the picture model object.

Download e1/views/app/controllers/upload_controller.rb

def show

@picture = Picture.find(params[:id]) end

In the template, the image tag links back to the action that returns the picture content. Figure 22.3 shows the get and show actions in all their glory.

Download e1/views/app/views/upload/show.rhtml

<h3><%= @picture.comment %></h3>

<img src="<%= url_for(:action => 'picture', :id => @picture.id) %>"/>

You can optimize the performance of this technique by caching the picture action. (We discuss caching starting on page 455.)

If you’d like an easier way of dealing with uploading and storing images, have a look at Rick Olson’s Acts as Attachment plugin.9 Create a database table that includes a given set of columns (documented on Rick’s site) and the plugin will automatically manage storing both the uploaded data and the upload’s metadata. Unlike our previous approach, it handles storing the uploads in both your filesystem or a database table.

And, if you’re uploading large files, you might want to show your users the status of the upload as it progresses. Have a look at the upload_progress plugin, which adds a new form_with_upload_progress helper to Rails.

9. http://technoweenie.stikipad.com/plugins/show/Acts+as+Attachment

Report erratum