Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 57 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
57
Dung lượng
1,39 MB
Nội dung
12.2 Manipulating avatars 377 eventual system call to the underlying convert executable will look something like this: system("#{convert} ") The appropriate value of convert (with full path name) will automatically be interpo- lated into the string used for the system call. 12.2.2 The save method Now that we know how to convert images, we come finally to the Avatar save method. save itself is somewhat of an anticlimax, since we push the hard work into an auxiliary function called successful_conversion?: Listing 12.13 app/models/avatar.rb class Avatar < ActiveRecord::Base # Image sizes IMG_SIZE = '"240x300>"' THUMB_SIZE = '"50x64>"' . . . # Save the avatar images. def save successful_conversion? end private . . . # Try to resize image file and convert to PNG. # We use ImageMagick's convert command to ensure sensible image sizes. def successful_conversion? # Prepare the filenames for the conversion. source = File.join("tmp", "#{@user.screen_name}_full_size") full_size = File.join(DIRECTORY, filename) thumbnail = File.join(DIRECTORY, thumbnail_name) # Ensure that small and large images both work by writing to a normal file. # (Small files show up as StringIO, larger ones as Tempfiles.) File.open(source, "wb") { |f| f.write(@image.read) } # Convert the files. system("#{convert} #{source} -resize #{IMG_SIZE} #{full_size}") system("#{convert} #{source} -resize #{THUMB_SIZE} #{thumbnail}") File.delete(source) if File.exists?(source) # No error-checking yet! return true end Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 378 Chapter 12: Avatars successful_conversion? looks rather long, but it’s mostly simple. We first define file names for the image source, full-size avatar, and thumbnail, and then we use the system command and our convert method to create the avatar images. We don’t need to create the avatar files explicitly, since convert does that for us. At the end of the function, we return true, indicating success, thereby following the same convention as Active Record’s save. This is bogus, of course, since the conversion may very well have failed; in Section 12.2.3 we’ll make sure that successful_conversion? lives up to its name by returning the failure status of the system command. The only tricky part of successful_conversion? touches on a question we haven’t yet answered: What exactly is an “image” in the context of a Rails upload? One might expect that it would be a Ruby File object, but it isn’t; it turns out that uploaded images are one of two slightly more exotic Ruby types: StringIO (string input-output) for images smaller than around 15K and Tempfile (temporary file) for larger images. In order to handle both types, we include the line File.open(source, "wb") { |f| f.write(@image.read) } to write out an ordinary file so that convert can do its business. 10 File.open opens a file in a particular mode— "wb" for “write binary” in this case—and takes in a block in which we write the image contents to the file using @image.read. (After the conversion, we clean up by deleting the source file with File.delete.) The aim of the next section is to add validations, but save already works as long as nothing goes wrong. By browsing over to an image file (Figure 12.3), we can update the hub with an avatar image of our choosing (Figure 12.4). 12.2.3 Adding validations You may have been wondering why we bothered to make the Avatar model a subclass of ActiveRecord::Base. The answer is that we wanted access to the error handling and validation machinery provided by Active Record. There’s probably a way to add this functionality without subclassing Active Record’s base class, but it would be too clever by half, probably only serving to confuse readers of our code (including ourselves). In any case, we have elected to use Active Record and its associated error object to implement validation-style error-checking for the Avatar model. 10 convert can actually work with tempfiles, but not with StringIO objects. Writing to a file in either case allows us to handle conversion in a unified way. Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 12.2 Manipulating avatars 379 Figure 12.3 Browsing for an avatar image. The first step is to add a small error check to the successful_conversion? func- tion. By convention, system calls return false on failure and true on success, so we can test for a failed conversion as follows: Listing 12.14 app/models/avatar.rb def successful_conversion? . . . # Convert the files. img = system("#{convert} #{source} -resize #{IMG_SIZE} #{full_size}") thumb = system("#{convert} #{source} -resize #{THUMB_SIZE} #{thumbnail}") File.delete(source) if File.exists?(source) # Both conversions must succeed, else it's an error. unless img and thumb errors.add_to_base("File upload failed. Try a different image?") return false end return true end Note that we have to use the return keyword so that the function returns immediately upon encountering an error. Also note that we’ve used errors.add_to_base Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 380 Chapter 12: Avatars Figure 12.4 The user hub after a successful avatar upload. rather than simply errors.add as we have before, which allows us to add an error message not associated with a particular attribute. In other words, errors.add(:image, "totally doesn't work") gives the error message “Image totally doesn’t work”, but to get an error message like “There’s no freaking way that worked” we’d have to use errors.add_to_base("There's no freaking way that worked") This validation alone is probably sufficient, since any invalid upload would trigger a failed conversion, but the error messages wouldn’t be very friendly or specific. Let’s explicitly check for an empty upload field (probably a common mistake), and also make sure that the uploaded file is an image that doesn’t exceed some maximum threshold Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 12.2 Manipulating avatars 381 (so that we don’t try to convert some gargantuan multigigabyte file). We’ll put these validations in a new function called valid_file?, and then call it from save: Listing 12.15 app/models/avatar.rb # Save the avatar images. def save valid_file? and successful_conversion? end private . . . # Return true for a valid, nonempty image file. def valid_file? # The upload should be nonempty. if @image.size.zero? errors.add_to_base("Please enter an image filename") return false end unless @image.content_type =~ /^image/ errors.add(:image, "is not a recognized format") return false end if @image.size > 1.megabyte errors.add(:image, "can't be bigger than 1 megabyte") return false end return true end end Here we’ve made use of the size and content_type attributes of uploaded images to test for blank or nonimage files. 11 We’ve also used the remarkable syntax if @image.size > 1.megabyte Does Rails really let you write 1.megabyte for one megabyte? Rails does. 11 The carat ^ at the beginning of the regular expression means “beginning of line,” thus the image content type must begin with the string "image". Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 382 Chapter 12: Avatars Since we’ve simply reused Active Record’s own error-handling machinery, allwe need to do to display error messages on the avatar upload page is to use error_messages_for as we have everywhere else in RailsSpace: Listing 12.16 app/views/avatar/upload.rhtml <h2>Avatar</h2> <% form_tag("upload", :multipart => true) do %> <fieldset> <legend><%= @title %></legend> <%= error_messages_for 'avatar' %> . . . <% end %> Now when we submit (for example) a file with the wrong type, we get a sensible error message (Figure 12.5). 12.2.4 Deleting avatars The last bit of avatar functionality we want is the ability to delete avatars. We’ll start by adding a delete link to the upload page (which is a sensible place to put it since that’s where we end up if we click “edit” on the user hub): Listing 12.17 app/views/avatar/upload.rhtml . . . <%= avatar_tag(@user) %> [<%= link_to "delete", { :action => "delete" }, :confirm => "Are you sure?" %>] . . . We’ve added a simple confirmation step using the :confirm option to link_to. With the string argument as shown, Rails inserts the following bit of JavaScript into the link: [<a href="/avatar/delete" onclick="return confirm('Are you sure?');">delete</a>] Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 12.2 Manipulating avatars 383 Figure 12.5 The error message for an invalid image type. This uses the native JavaScript function confirm to verify the delete request (Figure 12.6). Of course, this won’t work if the user has JavaScript disabled; in that case the request will immediately go through to the delete action, thereby destroying the avatar. C’est la vie. As you might expect, the delete action is very simple: Listing 12.18 app/controllers/avatar controller.rb . . . # Delete the avatar. def delete user = User.find(session[:user_id]) user.avatar.delete flash[:notice] = "Your avatar has been deleted." Continues Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 384 Chapter 12: Avatars Figure 12.6 Confirming avatar deletion with JavaScript. redirect_to hub_url end end This just hands the hard work off to the delete method, which we have to add to the Avatar model. The delete method simply uses File.delete to remove both the main avatar and the thumbnail from the filesystem: Listing 12.19 app/models/avatar.rb . . . # Remove the avatar from the filesystem. Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 12.2 Manipulating avatars 385 def delete [filename, thumbnail_name].each do |name| image = "#{DIRECTORY}/#{name}" File.delete(image) if File.exists?(image) end end private . . . Before deleting each image, we check to make sure that the file exists; we don’t want to raise an error by trying to delete a nonexistent file if the user happens to hit the /avatar/delete action before creating an avatar. 12.2.5 Testing Avatars Writing tests for avatars poses some unique challenges. Following our usual practice post-Chapter 5, we’re not going to include a full test suite, but will rather highlight a particularly instructive test—in this case, a test of the avatar upload page (including the delete action). Before even starting, we have a problem to deal with. All our previous tests have written to a test database, which automatically avoid conflicts with the development and production databases. In contrast, since avatars exist in the filesystem, we have to come up with a way to avoid accidentally overwriting or deleting files in our main avatar directory. Rails comes with a temporary directory called tmp, so let’s tell the Avatar model to use that directory when creating avatar objects in test mode: Listing 12.20 app/models/avatar.rb class Avatar < ActiveRecord::Base . . . # Image directories if ENV["RAILS_ENV"] == "test" URL_STUB = DIRECTORY = "tmp" else URL_STUB = "/images/avatars" Continues Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 386 Chapter 12: Avatars DIRECTORY = File.join("public", "images", "avatars") end . . . This avoids clashes with any files that might exist in public/images/avatars. Our next task, which is considerably more difficult than the previous one, is to simulate uploaded files in the context of a test. Previous tests of forms have involved posting information like this: post :login, :user => { :screen_name => user.screen_name, :password => user.password } What we want for an avatar test is something like post :upload, :avatar => { :image => image } But how do we make an image suitable for posting? The answer is, it’s difficult, but not impossible. We found an answer on the Rails wiki ( http://wiki.rubyonrails.org/), and have placed the resulting uploaded_file function in the test helper: Listing 12.21 app/test/test helper.rb # Simulate an uploaded file. # From http://wiki.rubyonrails.org/rails/pages/HowtoUploadFiles def uploaded_file(filename, content_type) t = Tempfile.new(filename) t.binmode path = RAILS_ROOT + "/test/fixtures/" + filename FileUtils.copy_file(path, t.path) (class << t; self; end).class_eval do alias local_path path define_method(:original_filename) {filename} define_method(:content_type) {content_type} end return t end We are aware that this function may look like deep black magic, but sometimes it’s important to be able to use code that you don’t necessarily understand—and this is one of those times. The bottom line is that the object returned by uploaded_file can be posted inside a test and acts like an uploaded image in that context. Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com [...]... email again in Chapter 14, where it will be a key component in the machinery for establishing friendships between RailsSpace users 13.1 Action Mailer Sending email in Rails is easy with the Action Mailer package Rails applies the MVC architecture to email, with an Action Mailer class playing the part of model Constructing a message involves defining a method for that message—reminder, for example—that... @user image = uploaded_file( "rails. png", "image/png") post :upload, :avatar => { :image => image } assert_response :redirect assert_redirected_to hub_url assert_equal "Your avatar has been uploaded.", flash[:notice] assert @user.avatar.exists? post :delete assert !@user.avatar.exists? end end Here we’ve tested both avatar upload and deletion Running the test gives > ruby test/functional/avatar_controller_test.rb... email actions on RailsSpace, starting with a remind action: > ruby script/generate controller Email remind exists app/controllers/ exists app/helpers/ Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 13.1 Action Mailer create exists create create create create 393 app/views/email test/functional/ app/controllers/email_controller.rb test/functional/email_controller_test.rb app/helpers/email_helper.rb... body access to the user information @body["user"] = user @recipients = user.email @from = 'RailsSpace ' we can write subject body recipients from 'Your login information at RailsSpace. com' {"user" => user} user.email 'RailsSpace ' The argument to deliver_message in the correspond action above is a hash containing the information needed to construct... challenging data modeling problem we’ve encountered so far To solve it, we’ll have to learn about simulated table names, foreign keys, and the has_many database association Adding and managing friendships will then take place through both email and web interfaces By the end of the chapter, we’ll be in a position to put a list of friends on the hub and user profile, making use of the avatar thumbnails... out that we can also accomplish this by writing @body = {"user" => user} or the even pithier body user In this last example, body is a Rails function that sets the @body variable Similar functions exist for the other mail variables, and together they make for an alternate way to define methods in Action Mailer classes This means that instead of writing @subject = 'Your login information at RailsSpace. com'... Chapter 13: Email Listing 13.5 app/models/user mailer.rb class UserMailer < ActionMailer::Base def reminder(user) @subject = 'Your login information at RailsSpace. com' @body = {} # Give body access to the user information @body["user"] = user @recipients = user.email @from = 'RailsSpace ' end end Action Mailer uses the instance variables inside reminder to construct a valid... require 'avatar_controller' # Re-raise errors caught by the controller class AvatarController; def rescue_action(e) raise e end; end class AvatarControllerTest < Test::Unit::TestCase fixtures :users def setup @controller = AvatarController.new @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new @user = users(:valid_user) end def test_upload_and_delete authorize... ActionMailer::Base Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 13.2 Double-blind email system 405 def message(mail) subject mail[:message].subject from 'RailsSpace ' recipients mail[:recipient].email body mail end end By writing body mail we automatically make everything in the mail hash available in the message template, with an instance variable... inbox within a few seconds; if it doesn’t, double-check the configuration in config/environment.rb to make sure that they correspond to your ISP’s settings Even if you can’t get your system to send email, automated testing will still probably work Unit and functional tests don’t depend on the particulars of your configuration, but rather depend on Rails being able to create UserMailer objects and simulate . between RailsSpace users. 13.1 Action Mailer Sending email in Rails is easy with the Action Mailer package. Rails applies the MVC architecture to email, with an Action Mailer class playing the part. problem to deal with. All our previous tests have written to a test database, which automatically avoid conflicts with the development and production databases. In contrast, since avatars exist. 13.1.1. By default, Rails email messages get sent as plain text; see http://wiki.rubyonrails.org /rails/ pages/HowToSendHtmlEmailsWithActionMailer for instructions on how to send HTML mail using Rails. Simpo