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

ITERATION F4: A SIDEBAR, MORE ADMINISTRATION 168

depot> rake db:sessions:clear

Navigate to http://localhost:3000/admin/list. The filter method intercepts us on the way to the product listing and shows us the login screen instead.

We show our customer and are rewarded with a big smile and a request: could we add a sidebar and put links to the user and product administration stuff in it? And while we’re there, could we add the ability to list and delete administrative users? You betcha!

11.4Iteration F4: A Sidebar, More Administration

Let’s start with the sidebar. We know from our experience with the order controller that we need to create a layout. A layout for the admin controller would be in the file admin.rhtml in the app/views/layouts directory.

Download depot_q/app/views/layouts/admin.rhtml

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" >

<html>

<head>

<title>Administer the Bookstore</title>

<%= stylesheet_link_tag "scaffold", "depot", :media => "all" %>

</head>

<body id="admin"> <div id="banner">

<img src="/images/logo.png" />

<%= @page_title || "Pragmatic Bookshelf" %>

</div>

<div id="columns"> <div id="side">

<p>

<%= link_to "Products", :controller => 'admin', :action => 'list' %>

</p>

<p>

<%= link_to "List users", :controller => 'login', :action => 'list_users' %>

<br/>

<%= link_to "Add user", :controller => 'login', :action => 'add_user' %>

</p>

<p>

<%= link_to "Logout", :controller => 'login', :action => 'logout' %>

</p>

</div>

<div id="main">

<% if flash[:notice] -%>

<div id="notice"><%= flash[:notice] %></div> <% end -%>

<%= yield :layout %>

</div>

</div>

</body>

</html>

Report erratum

ITERATION F4: A SIDEBAR, MORE ADMINISTRATION 169

We added links to the various administration functions to the sidebar in the layout. Let’s implement them now.

Listing Users

Adding a user list to the login controller is easy. The controller action sets up the list in an instance variable.

Download depot_q/app/controllers/login_controller.rb

def list_users

@all_users = User.find(:all) end

We display the list in the list_users.rhtml template. We add a link to the delete_user action to each line—rather than have a delete screen that asks for a user name and then deletes that user, we simply add a delete link next to each name in the list of users.

Download depot_q/app/views/login/list_users.rhtml

<h1>Administrators</h1> <ul>

<% for user in @all_users %>

<li><%= link_to "[X]", { # link_to options :controller => 'login', :action => 'delete_user', :id => user},

{# html options :method => :post,

:confirm => "Really delete #{user.name}?"

} %>

<%= h(user.name) %>

</li>

<% end %>

</ul>

Would the Last Admin to Leave...

The code to delete a user is simple. The login controller’s delete_user action is called with the user to delete identified by the id parameter. All it has to do is something like

def delete_user if request.post?

user = User.find(params[:id]) user.destroy

end

redirect_to(:action => :list_users) end

(Why do we check for an HTTP POST request? It’s a good habit to get into. Requests that change the server state should be sent using POST, not GET requests. That’s why we overrode the link_to defaults in the form and made

Report erratum

ITERATION F4: A SIDEBAR, MORE ADMINISTRATION 170

Figure 11.2: Listing Our Users

it generate a POST. But that works only if the user has JavaScript enabled. Adding a test to the controller finds this case and ignores the request.)

Let’s play with this. We bring up the list screen that looks something like Figure 11.2 and click the X next to dave to delete that user. Sure enough, our user is removed. But to our surprise, we’re then presented with the login screen instead. We just deleted the only administrative user from the system. When the next request came in, the authentication failed, so the application refused to let us in. We have to log in again before using any administrative functions. But now we have an embarrassing problem: there are no administrative users in the database, so we can’t log in.

Fortunately, we can quickly add a user to the database from the command line. If you invoke the command script/console, Rails invokes Ruby’s irb utility, but it does so in the context of your Rails application. That means you can interact with your application’s code by typing Ruby statements and looking at the values they return. We can use this to invoke our user model directly, having it add a user into the database for us.

depot> ruby script/console

Loading development environment.

>> User.create(:name => 'dave', :password => 'secret', :password_confirmation => 'secret')

=> #<User:0x2933060 @attributes={...} ... > >> User.count

=> 1

The >> sequences are prompts: after the first we call the User class to create

Report erratum

ITERATION F4: A SIDEBAR, MORE ADMINISTRATION 171

a new user, and after the second we call it again to show that we do indeed have a single user in our database. After each command we enter, script/console displays the value returned by the code (in the first case, it’s the model object, and in the second case the count).

Panic over—we can now log back in to the application. But how can we stop this from happening again? There are several ways. For example, we could write code that prevents you from deleting your own user. That doesn’t quite work—in theory A could delete B at just the same time that B deletes A. Instead, let’s try a different approach. We’ll delete the user inside a database transaction. If after we’ve deleted the user there are then no users left in the database, we’ll roll the transaction back, restoring the user we just deleted.

To do this, we’ll use an Active Record hook method. We’ve already seen one of these: the validate hook is called by Active Record to validate an object’s state. It turns out that Active Record defines 20 or so hook methods, each called at a particular point in an object’s life cycle. We’ll use the after_destroy hook, which is called after the SQL delete is executed. It is conveniently called in the same transaction as the delete, so if it raises an exception, the transaction will be rolled back. The hook method looks like this.

Download depot_q/app/models/user.rb

def after_destroy

if User.count.zero?

raise "Can't delete last user" end

end

The key concept here is the use of an exception to indicate an error when deleting the user. This exception serves two purposes. First, because it is raised inside a transaction, an exception causes an automatic rollback. By raising the exception if the users table is empty after the deletion, we undo the delete and restore that last user.

Second, the exception signals the error back to the controller, where we use a begin/end block to handle it and report the error to the user in the flash.

(In fact, this code still has a potential timing issue—it is still possible for two administrators each to delete the last two users if their timing is right. Fixing this would require more database wizardry that we have space for here.)

Logging Out

Our administration layout has a logout option in the sidebar menu. Its implementation in the login controller is trivial.

Report erratum

ITERATION F4: A SIDEBAR, MORE ADMINISTRATION 172

Download depot_q/app/controllers/login_controller.rb

def logout session[:user_id] = nil

flash[:notice] = "Logged out" redirect_to(:action => "login")

end

We call our customer over one last time, and she plays with the store application. She tries our new administration functions and checks out the buyer experience. She tries to feed bad data in. The application holds up beautifully. She smiles, and we’re almost done.

We’ve finished adding functionality, but before we leave for the day we have one last look through the code. We notice a slightly ugly piece of duplication in the store controller. Every action apart from empty_cart has to find the user’s cart in the session data. The line

@cart = find_cart

appears all over the controller. Now that we know about filters, we can fix this. We’ll change the find_cart method to store its result directly into the @cart instance variable.

Download depot_q/app/controllers/store_controller.rb

def find_cart

@cart = (session[:cart] ||= Cart.new) end

We’ll then use a before filter to call this method on every action apart from empty_cart.

Download depot_q/app/controllers/store_controller.rb

before_filter :find_cart, :except => :empty_cart

This lets us remove the rest of the assignments to @cart in the action methods. The final listing is shown starting on page 658.

What We Just Did

By the end of this iteration we’ve done the following.

Created a user model and database table, validating the attributes. It uses a salted hash to store the password in the database. We created a virtual attribute representing the plain-text password and coded it to create the hashed version whenever the plain-text version is updated.

Manually created a controller to administer users and investigated the single-action update method (which takes different paths depending on whether it is invoked with an HTTP GET or POST). We used the form_for helper to render the form.

Report erratum

ITERATION F4: A SIDEBAR, MORE ADMINISTRATION 173

We created a login action. This used a different style of form—one without a corresponding model. We saw how parameters are communicated between the view and the controller.

We created an application-wide controller helper method in the ApplicationController class in the file application.rb in app/controllers.

We controlled access to the administration functions using before filters to invoke an authorize method.

We saw how to use script/console to interact directly with a model (and dig us out of a hole after we deleted the last user).

We saw how a transaction can help prevent deleting the last user.

We used another filter to set up a common environment for controller actions.

Playtime

Here’s some stuff to try on your own.

Adapt the checkout code from the previous chapter to use a single action, rather than two.

When the system is freshly installed on a new machine, there are no administrators defined in the database, and hence no administrator can log on. But, if no administrator can log on, then no one can create an administrative user. Change the code so that if no administrator is defined in the database, any user name works to log on (allowing you to quickly create a real administrator).2

Experiment with script/console. Try creating products, orders, and line items. Watch for the return value when you save a model object—when validation fails, you’ll see false returned. Find out why by examining the errors:

>> prd = Product.new

=> #<Product:0x271c25c @new_record=true, @attributes={"image_url"=>nil, "price"=>#<BigDecimal:2719a48,'0.0',4(8)>,"title"=>nil,"description"=>nil}>

>>prd.save => false

>>prd.errors.full_messages

=> ["Image url must be a URL for a GIF, JPG, or PNG image", "Image url can't be blank", "Price should be at least 0.01", "Title can't be blank", "Description can't be blank"]

(You’ll find hints at http://wiki.pragprog.com/cgi-bin/wiki.cgi/RailsPlayTime)

2. Later, in Section 16.4, Data Migrations, on page 274, we’ll see how to populate database tables as part of a migration.

Report erratum