Agile Web Development with Rails phần 3 pps

55 382 0
Agile Web Development with Rails phần 3 pps

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

Thông tin tài liệu

ITERATION D1: CAPTURING AN ORDER 101 This method has to 1. Capture the values from the form to populate a new Order model object. 2. Add the line items from our cart to that order. 3. Validate and save the order. If this fails, display the appropriate mes- sages and let the user correct any problems. 4. Once the order is successfully saved, redisplay the catalog page, including a message confirming that the order has been placed. The method ends up looking something like this. File 30 Line 1 def save_order - @cart = find_cart - @order = Order.new(params[:order]) - @order.line_items << @cart.items 5 if @order.save - @cart.empty! - redirect_to_index('Thank you for your order.') - else - render(:action => 'checkout') 10 end - end Online3,wecreateanewOrder object and initialize it from the form data. In this case we want all the form data related to order objects, so we select the :order hash from the parameters (we’ll talk about how forms are linked to models on page 341). The next line adds into this order the line items that are already stored in the cart—the session data is still there throughout this latest action. Notice that we didn’t have to do anything special with the various foreign key fields, such as setting the order_id column in the line item rows to reference the newly created order row. Rails does that knitting for us using the has_many() and belongs_to() declarations we added to the Order and LineItem models. Next, on line 5, we tell the order object to save itself (and its children, the line items) to the database. Along the way, the order object will perform validation (but we’ll get to that in a minute). If the save succeeds, we empty out the cart ready for the next order and redisplay the catalog, using our redirect_to_index( ) method to display a cheerful message. If instead the save fails, we redisplay the checkout form. One last thing before we call our customer over. Remember when we showed her the first product maintenance page? She asked us to add validation. We should probably do that for our checkout page too. For now we’ll just check that each of the fields in the order has been given a Report erratum ITERATION D1: CAPTURING AN ORDER 102 Joe Asks. . . Aren’t You Creating Duplicate Orders? Joe’s concerned to see our controller creating Order model objects in two actions, checkout and save_order. He’s wondering why this doesn’t lead to duplicate orders in the database. The answer is simple: the checkout action creates an Order object in mem- ory simply to give the template code something to work with. Once the response is sent to the browser, that particular object gets abandoned, and it will eventually be reaped by Ruby’s garbage collector. It never gets close to the database. The save_order action also creates an Order object, populating it from the form fields. This object does get saved in the database. So, model objects perform two roles: they map data into and out of the database, but they are also just regular objects that hold business data. They affect the database only when you tell them to, typically by calling save(). value. We know how to do this—we add a validates_presence_of( ) call to the Order model. File 32 validates_presence_of :name, :email, :address, :pay_type So, as a first test of all of this, hit the Checkout button on the checkout page without filling in any of the form fields. We expect to see the checkout page redisplayed along with some error messages complaining about the empty fields. Instead, we simply see the checkout page—no error mes- sages. We forgot to tell Rails to write them out. 3 Any errors associated with validating or saving a model are stored with that model. There’s another helper method, error_messages_for(), that extracts and formats these in a view. We just need to add a single line to the start of our checkout.rhtml file. File 36 <%= error_messages_for("order")%> 3 If you’re following along at home and you get the message No action responded to save_order, it’s possible that you added the save_order( ) method after the private declaration in the controller. Private methods cannot be called as actions. Report erratum ITERATION D1: CAPTURING AN ORDER 103 Figure 9.2: Full House! Every Field Fails Validation Just as with the administration validation, we need to add the scaffold.css stylesheet to our store layout file to get decent formatting for these errors. File 35 <%= stylesheet_link_tag "scaffold", "depot", :media => "all" %> Once we do that, submitting an empty checkout page shows us a lot of highlighted errors, as shown in Figure 9.2 . If we fill in some data as shown at the top of Figure 9.3,onpage105,and click Checkout , we should get taken back to the catalog, as shown at the bottom of the figure. But did it work? Let’s look in the database. Report erratum ITERATION D2: SHOW CART CONTENTS ON CHECKOUT 104 depot> mysql depot_development Welcome to the MySQL monitor. Commands end with ; or \g. mysql> select * from orders; + + + + + + | id | name | email | address | pay_type | + + + + + + | 3 | Dave Thomas | customer@pragprog.com | 123 Main St | check | + + + + + + 1 row in set (0.00 sec) mysql> select * from line_items; + + + + + + | id | product_id | order_id | quantity | unit_price | + + + + + + | 4 | 4 | 3 | 1 | 29.95 | + + + + + + 1 row in set (0.00 sec) Ship it! Or, at least, let’s show it to our customer. She likes it. Except Do you suppose we could add a summary of the cart contents to the check- out page? Sounds like we need a new iteration. 9.2 Iteration D2: Show Cart Contents on Checkout In this iteration we’re going to add a summary of the cart contents to the checkout page. This is pretty easy. We already have a layout that shows the items in a cart. All we have to do is cut and paste the code across into the checkout view and ummm oh, yeah, you’re watching what I’m doing. OK, so cut-and-paste coding is out, because we don’t want to add dupli- cation to our code. What else can we do? It turns out that we can use Rails components to allow us to write the cart display code just once and invoke it from two places. (This is actually a very simple use of the compo- nent functionality; we’ll see it in more detail in Section 17.9, Layouts and Components,onpage356.) As a first pass, let’s edit the view code in checkout.rhtml to include a call to render the cart at the top of the page, before the form. File 38 <%= error_messages_for("order")%> <%= render_component(:action => "display_cart")%> <%= start_form_tag(:action => "save_order")%> <table> <tr> <td>Name:</td> The render_component( ) method invokes the given action and substitutes the output it renders into the current view. What happens when we run this code? Have a look at Figure 9.4,onpage106. Report erratum ITERATION D2: SHOW CART CONTENTS ON CHECKOUT 105 Figure 9.3: Our First Checkout Report erratum ITERATION D2: SHOW CART CONTENTS ON CHECKOUT 106 Figure 9.4: Methinks The Component Renders Too Much Oops! Invoking the display_cart action has substituted in the entire ren- dered page, including the layout. While this is interesting in a post- modern, self-referential kind of way, it’s probably not what our buyers were expecting to see. We’ll need to tell the controller not to use our fancy layout when it’s ren- dering the cart as a component. Fortunately, that’s not too difficult. We can set parameters in the render_component( ) call that are accessible in the action that’s invoked. We can use a parameter to tell our display_cart() action not to invoke the full layout when it’s being invoked as a compo- nent. It can override Rails’ default rendering in that case. The first step is to add a parameter to the render_component( ) call. File 40 <%= render_component(:action => "display_cart", :params => { :context => :checkout }) %> We’ll alter the display_cart( ) method in the controller to call different render methods depending on whether this parameter is set. Previously we didn’t have to render our layout explicitly; if an action method exits without calling a render method, Rails will call render( ) automatically. Now we need to override this, calling render(:layout=>false) in a checkout context. Report erratum ITERATION D2: SHOW CART CONTENTS ON CHECKOUT 107 File 39 def display_cart @cart = find_cart @items = @cart.items if @items.empty? redirect_to_index("Your cart is currently empty") end if params[:context] == :checkout render(:layout => false) end end When we hit Refresh in the browser, we see a much better result. We call our customer over, and she’s delighted. One small request: can we remove the Empty cart and Checkout options from the menu at the right? At the risk of getting thrown out of the programmers union, we say, “That’s not a problem.” After all, we just have to add some conditional code to the display_cart.rhtml view. File 42 <ul> <li><%= link_to 'Continue shopping' , :action => "index" %></li> <% unless params[:context] == :checkout -%> <li><%= link_to 'Empty cart' , :action => "empty_cart" %></li> <li><%= link_to 'Checkout', :action => "checkout" %></li> <% end -%> </ul> While we’re at it, we’ll add a nice-little heading just before the start of the form in the template checkout.rhtml in app/views/store. File 41 <%= error_messages_for("order")%> <%= render_component(:action => "display_cart", :params => { :context => :checkout }) %> <h3>Please enter your details below</h3> Report erratum ITERATION D2: SHOW CART CONTENTS ON CHECKOUT 108 A quick refresh in the browser, and we have a nice looking checkout page. Our customer is happy, our code is neatly tucked into our repository, and it’s time to move on. Next we’ll be looking at adding shipping functionality to Depot. What We Just Did In a fairly short amount of time, we did the following. • Added an orders table (with corresponding model) and linked them to the line items we’d defined previously • Created a form to capture details for the order and linked it to the Order model • Added validation and used helper methods to display errors back to the user • Used the component system to include the cart summary on the checkout page Report erratum Chapter 10 Task E: Shipping We’re now at the point where buyers can use our application to place orders. Our customer would like to see what it’s like to fulfill these orders. Now, in a fully fledged store application, fulfillment would be a large, com- plex deal. We might need to integrate with various backend shipping agen- cies, we might need to generate feeds for customs information, and we’d probably need to link into some kind of accounting backend. We’re not going to do that here. But even though we’re going to keep it simple, we’ll still have the opportunity to experiment with partial layouts, collections, and a slightly different interaction style to the one we’ve been using so far. 10.1 Iteration E1: Basic Shipping We chat for a while with our customer about the shipping function. She says that she wants to see a list of the orders that haven’t yet been shipped. A shipping person will look through this list and fulfill one or more orders manually. Once the order had been shipped, the person would mark them as shipped in the system, and they’d no longer appear on the shipping page. Our first task is to find some way of indicating whether an order has shipped. Clearly we need a new column in the orders table. We could make it a simple character column (perhaps with “Y” meaning shipped and “N” not shipped), but I prefer using timestamps for this kind of thing. If the column has a null value, the order has not been shipped. Otherwise, the value of the column is the date and time of the shipment. This way the column both tells us whether an order has shipped and, if so, when it shipped. ITERATION E1: BASIC SHIPPING 110 David Says. . . Date and Timestamp Column Names There’s a Rails column-naming convention that says datetime fields should end in _at and date fields should end in _on. This results in natural names for columns, such as last_edited_on and sent_at. This is the convention that’s picked up by auto-timestamping, described on page 267, where columns with names such as created_at are automat- ically filled in by Rails. So, let’s modify our create.sql file in the db directory, adding the shipped_at column to the orders table. File 47 create table orders ( id int not null auto_increment, name varchar(100) not null, email varchar(255) not null, address text not null, pay_type char(10) not null, shipped_at datetime null, primary key (id) ); We load up the new schema. depot> mysql depot_development <db/create.sql To save myself having to enter product data through the administration pages each time I reload the schema, I also took this opportunity to write a simple set of SQL statements that loads up the product table. It could be something as simple as lock tables products write; insert into products values(null, 'Pragmatic Project Automation', #title 'A really great read!', #description '/images/pic1.jpg', #image_url '29.95', #price '2004-12-25 05:00:00'); #date_available insert into products values( '', 'Pragmatic Version Control', 'A really controlled read!', '/images/pic2.jpg', '29.95', '2004-12-25 05:00:00'); unlock tables; Report erratum [...]... lot about Rails along the way, in a real-life project we might well have taken a different route The Rails generator facility can be extended—folks can create new generators for others to use If you look at the page that lists these add-ons ,3 you’ll see at least two off-the-shelf login controllers, both with a lot more functionality than the one we just wrote It might be prudent to experiment with these... for our application Report erratum 131 This chapter was written by Mike Clark (http:// clarkware.com) Mike’s an independent consultant, author, and trainer Most important, he’s a programmer He helps teams build better software faster using agile practices With an extensive background in J2EE and test-driven development, he’s currently putting his experience to work on Rails projects Chapter 12 Task T:... schema in the development database by hand, then you might not have a valid create.sql script No worries— Rails has a handy mechanism for cloning the structure (without the data) from the development database into the test database.1 Simply issue the following command in your application’s top-level directory (We’ll have more to say about all this rake business in Section 12.6, Running Tests with Rake,... URL the user requested so we can hop back to it # after login session[:jumpto] = request.parameters redirect_to(:controller => "login", :action => "login") end end 3 http://wiki.rubyonrails.com /rails/ show/AvailableGenerators Report erratum 130 M ORE I CING ON THE C AKE Then, once the login is successful, use these saved parameters in a redirect to take the browser to the page the user originally intended... pending orders File 53 def self.count_pending count("shipped_at is null") end Now we can experience the joy of logging in as an administrator Report erratum 124 I TERATION F3: L IMITING A CCESS We show our customer where we are, but she points out that we still haven’t controlled access to the administrative pages (which was, after all, the point of this exercise) 11 .3 Iteration F3: Limiting Access We... Testing In short order we’ve developed a respectable web- based shopping cart application Along the way, we got rapid feedback by writing a little code, and then punching buttons in a web browser (with our customer by our side) to see if the application behaves as we expected This testing strategy works for about the first hour you’re developing a Rails application, but soon thereafter you’ve amassed... code is sprinkled throughout the application You can never seem to test the smallest chunk of code without first firing up the database and then spoon-feeding it enough data to make the code do something interesting We programmers have a marketing term for that— bad coupling Report erratum 133 T ESTING M ODELS Rails promotes good testing (and good design) by enforcing a structure for your application whereby... because it means if we’ve already been testing our Ruby programs with Test::Unit tests (and why wouldn’t you want to?), then we can build on that knowledge to test Rails applications If you’re new to Test::Unit, don’t worry We’ll take it slow Now, what’s with the generated code inside of the test case? Well, when the Product model was created, Rails thought it would be a good idea if we actually tested... download Rails code generators which will write user management code for you Search the Rails wiki (wiki.rubyonrails.com) for login generator The Salted Hash version is the most secure from brute-force attacks Report erratum 119 I TERATION F1: A DDING U SERS incoming request If it has no associated data, it will come in as a GET request If instead it contains form data, we’ll see a POST Inside a Rails. .. can run tests again Rails has the answer—test fixtures 1 Currently supported only when using MySQL, PostgreSQL, or SQLite Report erratum 135 T ESTING M ODELS Test Fixtures The word fixture means different things to different people In the world of Rails, a test fixture is simply a specification of the initial contents of a model So, if we want to ensure that our products table starts off with the correct . along with some error messages complaining about the empty fields. Instead, we simply see the checkout page—no error mes- sages. We forgot to tell Rails to write them out. 3 Any errors associated with. mysql depot _development Welcome to the MySQL monitor. Commands end with ; or g. mysql> select * from orders; + + + + + + | id | name | email | address | pay_type | + + + + + + | 3 | Dave Thomas. them with a leading underscore in the filename. In this case, Rails will look for the partial in the file app/views/admin/_order_line.rhtml. The "order_line" parameter also tells Rails

Ngày đăng: 07/08/2014, 00:22

Từ khóa liên quan

Tài liệu cùng người dùng

  • Đang cập nhật ...

Tài liệu liên quan