This week we will continue our look at Rails with a consideration of:
We've already seen that we can retrieve a record by ID with a command such as:
@hit = Hit.find(params[:id])This code will find the hit from the hits table with an ID equal to the id parameter passed to the controller. However, what if we want to do a more specialised search, such as searching for all songs matching an artist that the user entered? Here is how we would do this. Imagine we had a view for the search controller method of the hits controller called search.rhtml which looked like this:
<% form_tag action=>'do_search' do %> Artist: < input name="artist" /> <input type="submit" value="Go!" /> endHow might we write a controller to respond to this view? It would look something like this:
class HitsController < ApplicationController
...
def do_search
@hit = Hit.find(:first,
[conditions=>"artist=:artist",params])
redirect_to action=> show, id=> @hit.id
end
end
How is this working? We pass two parameters to the find method
of Hit this time:
["artist=:artist",params]
["artist=:artist and title=:title",params]Here, the :artist in the condition would be substituted by the member of the params hash with a key of artist, and the :title in the condition would be subsituted by the member of the params hash with a key of title.
The find method returns an object of the Hit class. In this example we redirect straight to the view, but what if we wanted to access the individual fields of the Hit object returned? This is easy, we just use the database column names, e.g:
@hit.title @hit.artist @hit.year @hit.quantityetc. For instance we could check a hit is in stock via:
if @hit.quantity <= 0
# code to handle hit being out of stock
end
We can do this in the view as well as the controller: for instance we
might want to, in the view, inform the user that a given hit is out of stock.
Adding new records is easy. For example we could create a new Hit using this code:
@hit = Hit.new (:title => "Run",
:artist => "Snow Patrol",
:year => 2004,
:chart => 4)
@hit.save
This code would create a new song (note how we specify the database column
names preceded by a :) and save it to the database.
Updating an existing record is also easy. Imagine we've found a Hit, @hit, using find, as above, we can update either individual columns as follows:
@hit.chart = 3 @hit.save(this example would update the chart column for that hit to 3) or update several using update_attributes, as follows:
@hit.update_attributes (:chart => 3, :price => 0.79, :quantity => 2) @hit.saveRemember to always save any changes to the database after making them!
Deleting a record is straightforward also. Again, imagine we have found a hit, @hit, using find, we can simply do:
@hit.destroyOr, we can delete by ID without having to search first:
Hit.destroy(213)or using a query, which works in the same way as find:
Hit.destroy_all(["artist=:artist", params])
We'll now move onto the other new Rails topic for this week, namely how to maintain state with session variables using Rails. We'll use a login system to illustrate this, where the user's username is saved in a session variable upon correct login. Then, in any pages which require a user to be logged in, we can check for the existence of this session variable. Luckily, session variables in Rails are not too dissimilar to those in PHP!
We'll start by writing a login script which checks a username and password against the correct values in the database. We can use the same sort of technique that we did for the search example, above, by checking whether there is a row in the database containing the username and password that the user entered.
We could use a 'users' controller here, analagous to the 'hits' controller used above. We would create a database table called 'users', and generate, using a scaffold, a users controller (similar to the hits controller from last week) which contains, by default, methods for adding, viewing, editing and deleting users. Then, we could add two further methods:
http://localhost:3000/user/login http://localhost:3000/user/validatewith the first URL bringing up the login form.
CREATE TABLE users (id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(255), password VARCHAR(255), isadmin INT DEFAULT 0);
ruby script/generate scaffold UserThis will generate a scaffold for manipulating users, similar to the scaffold for manipulating hits last week.
cd app/views/user>first.
<html>
<head>
<style type="text/css">
body { font-family: helvetica, arial, sans-serif }
.error { color: red }
</style>
</head>
<body>
<h1>Login</h1>
<div class="error">
<% if flash[:notice] %>
<%= flash[:notice] %>
<% end %>
</div>
<% form_tag :action => 'validate' do %>
<p><label for="username">Username</label>
<input name="username" id="username" /> <br />
<p><label for="password">Password</label>
<input type="password" name="password" id="password" /> <br />
<input type="submit" value="Go!" />
<% end %>
</body>
</html>
Note how most of this is just HTML but:
Next we'll create the validate method. Move to the controllers folder:
cd ../../controllersand edit users_controller.rb and add a validate method. (Note that we do not have to create a controller method for the login view. For some actions, such as displaying the login form, the controller doesn't have to have any custom behaviour. If we omit the controller method for a particular action, Rails will simply bring up the corresponding view).
def validate
@user = User.find(:first,
:conditions => ["username=:username and password=:password", params])
if ! @user
flash[:notice] = 'Invalid login.'
redirect_to :action => 'login'
else
session[:gatekeeper] = @user.username
redirect_to '/hits/list'
end
end
Note here:
As you should already know from your experience with PHP or other server side languages, this is not the whole story. We need to prevent people bypassing the login system by entering the URL of one of the website's internal pages. We can do this just as easily in Rails as we can in PHP. Move back to the views:
cd ../views/hitsand edit list.rhtml so it looks like this:
<% if ! session[:gatekeeper] %> You're not logged in. Go away! <% else %> Logged in as: <%= session[:gatekeeper] %> (the rest of the page goes here) <% end %>Hopefully this should be obvious: we check for the existence of the session variable; if it does not exist we tell the user to go away, otherwise we display the page.
The final part of the equation is the logout script, to allow a logged-in user to logout when they have finished their session with the website. This is quite simple. First add a link to the list.rhtml view for hits:
link_to 'logout', '/users/logout'and then create a logout method in the users controller. Move back to the controllers folder:
cd ../../controllersand edit the users_controller.rb file:
def logout
reset_session
redirect_to :action => 'login'
end
This is quite simple. The reset_session line clears the current
session (and therefore destroys all session variables), and we then redirect
back to the login form.
Test the login system by:
In our discussion we have somewhat neglected the role of the model, focusing instead on the controller and the view. Remember that the model is the code which communicates directly with the database. We have used the model above when we obtained a Hit object or a User object from a database query: the Hit and User objects we obtained were part of the model.
However what about adding our own methods to model classes? When should we do this? Typically you add methods to the model when you want to define custom behaviour for communication between the model and the database; something more specific than the standard creation, search, update and destroy methods discussed above. Such methods would typically add extra error handling, specific to our system, and provide a higher level interface to application developers compared with the built in Rails database manipulation methods above Good examples, some of which you have seen already, would be: