Thursday, November 12, 2009

Ramaze - Forgot Password (Edited 2009-11-20)

Edit: I received some rather stern comments on this post on the Sequel mail list and probably rather deservedly. The primary complaints were that you really shouldn't even be doing this sort of thing as it a) involves saving the user's password as plaintext in the database and b) sending the user's password in plaintext over the internet via email. The solution is to save the user's password as a hash in the database and then provide a link to them for changing the password if requested. I basically agree with the criticism, so use this only for non-critical applications (i.e. don't use if for a banking app.) or better yet, just use some of the techniqes (email, AJAX) without using the whole idea. Thanks to all who commented to set me straight.

-----------------------------------------------------

For a recent project, I was looking at how to notify the user if they forgot their password. I'd initially thought the interesting piece would be the email part, but really, not so much. Let's start there though. The first thing you'll need to add that we haven't before is install Michael Fellinger's (Manveru) Mailit. So ...
sudo gem install mailit

With that taken care of, let's take a look at our database migrations. There's two of them since I added the admins table later. Here's the first one

# dbMigration/001_ForgotPassword.rb
#
# This is the "new" way to do migrations by using the Class.new form.
# It means that a new class name is not required and so there is no chance
# of creating a second class in the migration files that is the same as the
# first causing problems.
Class.new(Sequel::Migration) do
def up
# Create the users table.
create_table(:users) do
primary_key :id
String :user_name
String :password
String :email
String :challenge_answer
foreign_key :challenge_question_id, :challenge_questions
end

# Create the challenge questions table.
create_table(:challenge_questions) do
primary_key :id
String :question
end

end

def down
# Remove the two tables.
drop_table(:users, :challenge_questions)
end
end



Here we create a user table that has a user_name, password, email, and a challenge_response. It also has a foreign key to the challenge_questions table for the challenge question. The challenge_question table contains a list of potential questions that the user can select. This will be handled as a drop down when they register for the site.

The second migration script contains the admins table. For this application, about the only thing an admin can do is add more challenge questions. We'll also add in an admin since we don't have another way of doing it in this application. Here we'll give the admin the name "admin" (clever) and a password "helloworld" (better anyway), and an email address that's never used.

# dbMigration/002_ForgotPasswordMigration.rb
#
# This is the "new" way to do migrations by using the Class.new form.
# It means that a new class name is not required and so there is no chance
# of creating a second class in the migration files that is the same as the
# first causing problems.
Class.new(Sequel::Migration) do
def up
# Add in the admins table for administrators.
create_table(:admins) do
primary_key :id
String :admin_name
String :password
String :email
end

# Add in an administrator.
from(:admins).insert(:admin_name => 'admin', :password => 'helloworld', :email => 'admin@company.com')

end

def down
# Remove the administrators table.
drop_table(:admins)
end
end



We run our migrations with the following:

sequel -m dbMigration/ sqlite://forgot.db

The models we use for this project are pretty simple. The User model just states that it's many_to_one with the challenge questions (many users can have the same challenge question). The Admin model is about as simple as it gets (empty). The ChallengeQuestion model has the one_to_many with users (one challenge question can be associated with many users). It also has the self.questions method which will return all of the challenge questions in the database and their associated ids. This will be used by the registration page to allow the user to select one challenge question from a drop down.

# Create the User model. Each user can have a single challenge question.
class User < Sequel::Model
many_to_one :challenge_question
end

# Create the Challenge_Question model. Multiple users can have a single
# challenge question.
class ChallengeQuestion < Sequel::Model
one_to_many :users

# Will return an array of all of the questions.
def self.questions
select(:id, :question).all
end
end

# Create the Administrator model.
class Admin < Sequel::Model
end



So now we're ready to move on to the start.rb file.

# start.rb
#
# This is the main program for the example. It loads the Sequel database,
# loads the controllers and models, and then starts up Ramaze.
#
# The database should have been set up using the database migrations in
# the dbMigration directory.
require 'rubygems'
require 'ramaze'
require 'sequel'

# Required for mailing.
require 'net/smtp'
require 'mailit'

# Create the mailer.
MAILER = Mailit::Mailer.new(:server => 'MailServer.MyCompany.COM', :port => 25, :username => 'ApplicationName', :password => 'ApplicationPassword')

# Open the forgot password database. This must be done before we access the
# models that use it.
DB = Sequel.sqlite("forgot.db")

# Load the controllers and models.
require 'models/models'
require 'controllers/main_controller'
require 'controllers/admin_controller'

# Start Ramaze.
Ramaze.start



The difference here from most of our other start.rb files is the addition of the code for the mailer. First we require 'net/smtp' and 'mailit'. The we create the MAILER. We'll pass the server name, the port (will almost certainly be 25), the username, and the password. This will create a mailer that we can then "send" messages to. Next we open the database, forgot.db, and then get the models and the controllers.

Let's take a look at the admin controller first.

# controllers/admin_controller.rb
#
# The AdminController has a single method index. First map to /admin for
# the view. Next, set the layout to page so that we use the layout/page.xhtml for our
# layout. Then we use a helper for the :xhtml which will allow us to use "js" in the
# layout (we use this to generate a javascript link).
class AdminController < Ramaze::Controller
# The Admin controller will be accessed using "admin" as in:
# http://localhost:7000/admin.
map '/admin'

# Use page.xhtml in the layout directory for layout
layout :page

# Let's us put in our "js" lines.
helper(:xhtml)

# Set up a helper to check if we're logged in and only allow access
# to the :logged_in page if we are. This is probably the hard way to
# do this for only the single page but will make much more sense if
# we add more pages as we'd do in a real application.
helper :aspect
before(:main, :add_challenge) {
unless logged_in?
# Set the flash message which will only be available in the next
# screen. In this case that will be the logged_in screen.
flash[:message] = "You must log in before accessing the requested page."
redirect rs(:index)
end
}

# You can access it now with http://localhost:7000/admin
def index
Ramaze::Log.debug "Enter Admin Index"
@title = "SteamCode - Administration Home"
end

def main
end

# Add a new challenge question for the user to select.
def add_challenge
if request.post?
challenge_question = request[:challenge_question]
ChallengeQuestion.create(:question => challenge_question)
end
end

# Login as an administrator. Figure out if this is a correct login/password
# pair and log the admin in and redirect ot main if it is. If not, flash
# a message and redirect back to the login page.
def login
@title = "Library - Administration - Login"
if request.post?
if admin = Admin.find(:admin_name => request[:admin_name], :password => request[:password])
# Use the name= portion of the input form to grab the data
# from the request variable and save it in the session
# hash table.
session[:admin_id] = admin.id

# Redirect to the list_all screen.
redirect rs(:main)
else
# The login could not be authorized. Set the flash message
# and stay on this page (index/login). Set the session loginID
# to nil also. This will effectively log the user out. This would
# be reasonable if they are logged in and then try to log in with
# a new login/password.
flash[:message] = "Incorrect user name or password, please try again!!!"
session[:admin_id] = nil

# Stay on the login page.
redirect rs(:login)
end
end
end

# Log the administrator out.
def logout
session[:admin_id] = nil
flash[:message] = "Admin Logged out"
redirect MainController.r(:index)
end

private

# If the admin is logged in, the session will
# contain a non nil admin id.
def logged_in?
session[:admin_id] != nil
end

end



It starts out with most of the same code as all of our controllers. There's the map command, the layout (here we'll use layout/page.xhtml), the helper for our javascript, the helper for aspect (this will allow us to do before/after commands for selected methods in this controller), the before command which will have us check for being logged in before allowing access to the main page and the add_challenge page. The first method is the index method which is the page that the admin will go to before logging in. This is followed by main which is where the admin will be redirected after login. Next we have the add_challenge method. Here we just grab the question from the request hash (filled in by the add_challenge page input) and create a new ChallengeQuestion from it (also adding it to the database). Next is the login method, which should be pretty familiar from past posts. If we find the admin (and here the only one we have was created by the database migration), we'll set the session admin_id variable and redirect to the main page. If we fail to login the admin, we'll put up a flash message and redirect back to the login page. Next is the logout which just resets the session admin_id, sets a message, and redirects back to the login page. Finally, we have the private method logged_in? which will check if an admin is logged in.

Let's take a look at the admin views. First we have the view/admin/index.xhtml.

#{flashbox}
<p> Welcome to the Library. This is the Administrator's section. Please login and administrate. </p>



Nothing too much here, just a note for the user to login.

Next we have the main view.

<!-- The only thing here is a link to add a challenge question. -->
<h2>Enter Admin</h2>
<a href="#{r(:add_challenge)}">Add Challenge Question</a>



This is the page that the admin will see when they log in to the system. Here, there's just a link to the add challenge question page.

<form id="login" method="post">
<fieldset>
<legend> Add Challenge </legend>
<div>
<!-- for= goes with id=, the name= is placed in the request variable. -->

<!-- Input for the challenge_question. -->
<label for="challenge_question">User Name:</label>
<input id="challenge_question" name="challenge_question" type="text" />
<br/>

<!-- Submit the new challenge question (this should result in it being saved in the database) -->
<input type="submit" value="Add" />
</div>
</fieldset>
</form>



Here we just have the input box for the challenge question and the submit button. This will, as noted above, add a new challenge question to the database.

Here's the login page.

#{flashbox}
<form id="login" method="post">
<fieldset>
<legend> Login </legend>
<div>
<!-- for= goes with id=, the name= is placed in the request variable. -->

<!-- Input for the admin_name. -->
<label for="admin_name">Admin:</label>
<input id="admin_name" name="admin_name" type="text" />
<br/>

<!-- Input for the password. -->
<label for="password">Password:</label>
<input id="password" name="password" type="password" />
<br/>

<!-- Submit the admin name and password. If accepted,
the admin should get logged in. -->

<input type="submit" value="Login" />
</div>
</fieldset>
</form>



It has input boxes for the admin's admin_name and password as well as the submit button. Once again, nothing very interesting. And the end of the admin pages.

Let's turn to the user side now. First we have the user controller, controllers/main_controller.rb.

# controllers/main_controller.rb
#
# The mainController has a single method index. First map to /admin for
# the view. Next, set the layout to page so that we use the layout/page.xhtml for our
# layout. Then we use a helper for the :xhtml which will allow us to use "js" in the
# layout (we use this to generate a javascript link).
class MainController < Ramaze::Controller

# The Main controller will be accessed using "main" as in:
# http://localhost:7000/main.
map '/'

# Use page.xhtml in the layout directory for layout except
# for when we're doing AJAX.
layout(:page) { !request.xhr? }

# Let's us put in our "js" lines.
helper(:xhtml)

# Set up a helper to check if we're logged in and only allow access
# to the :logged_in page if we are. This is probably the hard way to
# do this for only the single page but will make much more sense if
# we add more pages as we'd do in a real application.
helper :aspect
before(:account_settings, :main) {
unless logged_in?
# Set the flash message which will only be available in the next
# screen. In this case that will be the logged_in screen.
flash[:message] = "You must log in before accessing the requested page."
redirect rs(:index)
end
}

# You can access it now with http://localhost:7000/
def index
@title = "SteamCode - User"
end

# Placeholder for real content.
def main
"<h2>Main</h2>"
end

# Placeholder for real content.
def about
"<h2>About</h2>"
end

# Placeholder for real content.
def help
"<h2>User Help</h2>"
end

# Register a new user with SteamCode. We will get here from the
# views/register.xhtml page where the user will put in their (requested)
# login, password, and email address. First find if the user already
# exists, if it does, then we'll set a message to tell the user so and
# redirect them back to the register screen. If not, we'll go ahead and add
# them to the database with the appropriate login, password, and email
# address. We'll then send them to the login screen to let them log in to
# SteamCode.
def register
@title = "Register with SteamCode"
@questions = ChallengeQuestion.questions
# Make sure we're getting here from a post request.
if request.post?
# Check the login and password.
# if we find the Account based on the login and password. If we find it
# we'll save the login ID in the session variable and we can use that
# to show if the Account is currently logged in or not. If we can't
# find the Account, we'll set the flash message, set the session to nil
# and just stay on this page.
if User.find(:user_name => request[:user_name])

# This user already exists. Set the flash message for them to
# try again.
flash[:message] = "Login #{request[:user_name]} already used. Please select another."

# Stay on the register page.
redirect rs(:register)
else
# This account does not exist. Grab the user_name, the password,
# and the email and create a new Account with them.
user_name = request[:user_name]
password = request[:password]
email = request[:email]

# Log the new user (a real application wouldn't probably print
# the password out though).
# Ramaze::Log.debug "New User Added: user_name = #{user_name} password = #{password} email = #{email}"

# Create the account with the user_name, password, and email given.
user = User.create(:user_name => user_name, :password => password, :email => email,
:challenge_answer => request[:challenge_answer],
:challenge_question => ChallengeQuestion[request[:challenge_question]])
Ramaze::Log.debug "New User Added: user_name = #{user.user_name} password = #{user.password} email = #{user.email} question = #{user.challenge_question.question}"

# Redirect to the login page.
redirect rs(:login)
end
end
end

# Login to Steamcode. If the request is a post, then we'll try to find the
# user. If we succeed then we'll set some session variables and redirect to
# the main page (which the user can only access if they're logged in). If they
# can't be logged in, we'll set a flash message, reset the session, and redirect
# back to the login page.
def login
if request.post?
if user = User.find(:user_name => request[:user_name], :password => request[:password])
# Use the name= portion of the input form to grab the data
# from the request variable and save it in the session
# hash table.
session[:user_id] = user.id
session[:user_name] = user.user_name

# Redirect to the main screen.
redirect rs(:main)
else
# The login could not be authorized. Set the flash message
# and stay on this page (index/login). Set the session loginID
# to nil also. This will effectively log the user out. This would
# be reasonable if they are logged in and then try to log in with
# a new login/password.
flash[:message] = "Incorrect user name or password, please try again!!!"
session[:user_id] = nil
session[:user_name] = nil

# Stay on the login page.
redirect rs(:login)
end
end
end

# Let the user change account settings. For now this is
# just the email and password.
def account_settings
user = User[session[:user_id]]
if request.post?
user = User[session[:user_id]]
user.email = request[:email]
user.password = request[:password]
user.save
flash[:message] = "New email and/or password saved."
redirect rs(:main)
end
@current_password = user.password
@current_email = user.email
end

# Logout of the system. Set the flash message and then
# set the session values to nil. Finally, redirect back to the
# index page.
def logout
flash[:message] = "#{session[:user_name]} Logged out"
session[:user_id] = nil
session[:user_name] = nil
redirect rs(:index)
end

# The user has requested that we email their password back to them. When they submit
# their challenge response and we verify it, we'll set up an email response using
# Mailit and send it to them.
def forgot_password
Ramaze::Log.debug "Enter Forgot Password"
@page_javascript = 'forgot_password'
if request.post?
Ramaze::Log.debug "Challenge Question Submitted: Email: #{request[:user_name]} Answer: #{request[:challenge_answer]}"
# Final submit.
if user = User.find(:user_name => request[:user_name], :challenge_answer => request[:challenge_answer])
Ramaze::Log.debug "Found user: #{user.user_name} #{user.email} #{user.password}"


# Create the mail message and fill it in with the appropriate
# information (to/from/subject/text). Then send it off.
mail = Mailit::Mail.new
mail.to = user.email
mail.from = "Steamcode@MyCompany.com"
mail.subject = "Steamcode Password"
mail.text = "Your password is: #{user.password}."

# Send the mail message via the MAILER (created in start.rb).
MAILER.send(mail)

# Just go back to the login page.
redirect rs(:login)
else
Ramaze::Log.debug "Could not find user: #{request[:user_name]} or incorrect challenge response."
flash[:message] = "Could not find user: #{request[:user_name]} or incorrect challenge response."

# Could not find user with this user_name/challenge answer just redirect to forgot password
redirect rs(:forgot_password)
end
end
end

# This is called from an Ajax request. We take in the email address that the
# user submitted and then pass back the challenge question for that user. If
# we can't find the user, we won't respond with anything and we'll let the
# javascript (public/js/forgot_password.js) deal with it. In this case, they'll
# just pop up an alert to let the user know.
def generate_forgot_question
if request.xhr?
# Get the user_name and if it exists, return the challenge question. If not, generate the
# could not find user_name messesage.
if user = User.find(:user_name => request[:user_name])
challenge_question = user.challenge_question.question
Ramaze::Log.debug "Challenge Question Requested: challenge_question = #{challenge_question}"

# It looks like we a) MUST use the respond command and b)MUST use the 200 return value. This was
# determined by just trying different things.
json = "{ challenge_question: \"#{challenge_question}\"}"
respond json, 200
else
# Go ahead and log a message.
Ramaze::Log.debug "Could not find user with user_name: #{request[:user_name]}"
end
end
end

private

# If the user is logged in, the session will
# contain a non nil user id.
def logged_in?
session[:user_id] != nil
end
end



The main controller starts the exact same way that the admin controller does. It has the map, layout, helper, and aspect lines and for all of the exact same reasons. Next is the index method which is the default page before someone logs in and this is followed by the main page which is where a user ends up after they log in. Next are two placeholder methods about and help that can be used for obvious purposes. Next is the registration method. We check if this is called from a post and if it is, we check to see if we already have a user with the user_name that was submitted. If we already have that name registered, we'll flash the user a message and send them back to the registration page. If we don't, we'll create a new user with the user_name, password, email, and challenge question/answer. Then we'll redirect them to the main page. Next, the login page will see if they can find the user with the given user_name and password and if so, we save their information in the session and redirect them to the main page. If not, we'll flash a message and redirect back to the login page so that they can try again. The account_settings allows the user to change their email and/or password. The logout method, like the corresponding admin logout, sets the session id and redirects the user back to the index page. The forgot_password method is the main reason for the post and it's actually pretty simple. We check the user_name and the challenge_answer that they provided and if they match we use the MAILER constant to send the email containing their password and redirect them to the login page. If we can't find the user or if the challenge answer doesn't match, we'll flash a message and send them back to the forgot_password page. The generate_forgot_question will come from an AJAX request. If we find the user_name, we'll send their challenge question back to them using JSON. If not, we'll just stay on the same page and they can try again. Finally, we have the logged_in? method, for checking if the user is logged in (obviously). We use this to protect certain pages from users who aren't logged in.

Now, let's take a look at the views. First is the index page and it's a simple Welcome message.

#{flashbox}
<h2>Welcome</h2>



Next, the registration page contains input boxes for the user_name, password, email, and challenge response. There's also a drop down for the challenge question and the submit button.

<!-- view/register.xhtml -->
<form id="register" method="post">
<div>
<!-- for= goes with id=, the name= is placed in the request variable. -->

<!-- Input for the user_name. -->
<label for="user_name">Login:</label>
<input id="user_name" name="user_name" type="text" />
<br/>

<!-- Input for the password. -->
<label for="password">Password:</label>
<input id="password" name="password" type="password" />
<br/>

<!-- Input for the email. -->
<label for="email">Email:</label>
<input id="email" name="email" type="text" />
<br/>

<!-- Input for the challenge question. The register() method will get the list
of challenge questions from the database table ChallengeQuestion and will
pass the list of questions and ids.
-->

<label for="challenge_question" class="label">Challenge Question:</label>
<select name="challenge_question">
<?r @questions.each do | question | ?>
<option value=#{question.id}>#{question.question} </option>
<?r end ?>
</select>
<br/>

<!-- Input for the challenge_answer. We'll save this and if they need to retrieve
their password, we'll ask the question from above and see if they know the
answer they will submit here.
-->

<label for="challenge_answer">Challenge Response:</label>
<input id="challenge_answer" name="challenge_answer" type="text" />
<br/>

<!-- Submit the new User -->
<input type="submit" value="Register" />
</div>
</form>



The login page only has the input boxes for the user_name and password along with the submit button.

#{flashbox}
<a href="#{r(:forgot_password)}">Forgot your password?</a>
<br/>

<form id="login" method="post">
<fieldset>
<legend> Login </legend>
<div>
<!-- for= goes with id=, the name= is placed in the request variable. -->

<!-- Input for the user_name. -->
<label for="user_name">User Name:</label>
<input id="user_name" name="user_name" type="text" />
<br/>

<!-- Input for the user_name. -->
<label for="password">Password:</label>
<input id="password" name="password" type="password" />
<br/>

<!-- Submit the login request. -->
<input type="submit" value="Login" />
</div>
</fieldset>
</form>



The account_settings page only has the input boxes for the email and password along with the submit button. It would actually be nice to have a way to change the challenge question/response also.

<!-- Let's the user change their email and/or password -->
<form id="change_password" method="post">
<fieldset>
<legend> Change Settings </legend>
<div>
<!-- Input for the email address. -->
<label for="email">Email:</label>
<input id="email" name="email" type="text" value=#{@current_email} />
<br/>

<!-- Input for the password. -->
<label for="password">Password:</label>
<input id="password" name="password" type="password" value=#{@current_password} />
<br/>

<!-- Submit the new email and/or password -->
<input type="submit" value="Submit" />
</div>
</fieldset>
</form>



The forgot_password page is reached from the login page. The user is offered a link for a forgotten password. Here they have an input box for their user name and then they'll put submit for and get their challenge question back. We then use a bit of AJAX magic to display the challenge question. When they put in their answer they can submit that and then get their password mailed to them as outlined above. Here's the XHTML followed by the JavaScript for the AJAX piece.

<h2>Forgot Password</h2>
#{flashbox}

<form id="forgot_screen" method="post">
<fieldset>
<legend> Forgot Password </legend>
<div id='forgot_password'>

<!-- for= goes with id=, the name= is placed in the request variable. -->
<!-- Input for the user_name. -->
<label for="user_name">User Name:</label>
<input id="user_name" name="user_name" type="text" value="User Name" />
<br/>

<!-- Button for getting the challenge question based on the -->
<!-- user_name above -->
<div id='challenge_question'>
<input type="submit" id="get_challenge" value="Get Challenge" />
</div>

</div>

<!-- Once they've put in the challenge answer, they will select -->
<!-- this and the system will send their password via email. -->
<input type="submit" value="Send Password" />
</fieldset>
</form>
<br/>



// public/js/forgot_password.js
$(document).ready(function() {
// Grab the get_challenge so we can add the choiceMarkup to it.
var user_name_container = $("#user_name");

// Add click handler. When the get_challenge button is clicked, we'll send the
// user_name value to the generate_forgot_question() method in the controller.
$("#get_challenge").click(function(e) {

// Don't do the normal thing you'd do when clicking a button.
e.preventDefault();

// Send a post request to the generate_forgot_question method when the
// get_challenge button is clicked. Pass in the JSON "user_name:
// user_name_container.val()" (value in the user_name input field to the
// generate_forgot_question() method. The callback routine gets a
// resultObject(JSON) and a status (not used and not actually
// returned). The generate_forgot_question() method will return JSON,
// the fourth parameter, to post.
$.post(
"/generate_forgot_question",
{user_name: user_name_container.val()},
function(resultObject, resultStatus) {

var answer_container = $("#challenge_answer");

/* Check if the result contains the correct json and that there is no answer_container already. */
if (answer_container.length == 0)
{
if (resultObject.challenge_question != undefined)
{
// Take the resultObject and grab the challenge_question from it and add the
// input for the user to submit.
var result = [
"
Challenge Question: ", resultObject.challenge_question, "
",
"",
"
"
];

// Add the result to the challenge_question at the bottom.
$("#challenge_question").append(result.join(''));
}
else
{
/* There wasn't a challenge answer returned, so let the user know. */
alert("Could not find user name " + user_name_container.val());
}
}
},
'json' );
});
});



I've tried to comment this pretty well, but let's go through it. As always with jQuery we're going to make sure the document is ready before doing anything. Next, we'll grab the user_name container. We're going to use it to get the name the user types in to it and pass it back to the server for processing. Next, we set up a click function for the get_challenge button. We do this so we can use it to send the user name and retrieve the challenge question. Next, we disable the normal thing (submit) that we'd do for a button. Then we set up the post to the generate_forgot_password() method in the main controller. We'll pass the user_name that we get from the user_name_container we grabbed above, set up a function for processing the return value, and finally we'll let everyone know we're passing JSON back as the return value. Now let's look at the processing function. First we check to see if we already have a challenge_answer. If we don't, we check the resultObject and see if we have a challenge_question. If we do, then we put the challenge question and then create an input box for the answer. We then join this new HTML to the end of the challenge_question. There may be (OK probably is) better ways to do this. I'm not really an expert on JavaScript or AJAX or JSON, so if you have suggestions for cleaning this up, please leave some hints in the comments.

Finally, let's take a look at the layout page.

<!-- view/page.xhtml -->

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

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<!-- Use the page.css in the public directory and set title based on
what's set in the associated method.
-->

<link rel="stylesheet" type="text/css" href="/page.css"/>

<!-- Serve jQuery from Google. This appears to be the accepted way of doing things now. -->
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"></script>

<!-- Need to have the xhtml helper for this next line -->
#{ js @page_javascript }

<title>#@title</title>
</head>
<body>
<div id="whole_page">
<div id="header">
<h1>SteamCode</h1>
</div>

<div id="nav">
<?r if action.node.to_s == "MainController" ?>
<!-- Main/User controller -->
<!-- Move this next section over to the right side of the screen. It
will contain the Login/Register if we're not logged in and the Logout
if we are. -->

<span style="float: right">
<!-- We're going to use the private method logged_in? here to test if
we want to show the Login/Register links or the Logout link -->

<?r if !logged_in? ?>
<a href="#{r(:login)}">Login</a>
<a href="#{r(:register)}">Register</a>
<?r else ?>
<a href="#{r(:account_settings)}">Account</a>
<a href="#{r(:logout)}">Logout</a>
<?r end ?>
</span>

<!-- These next three will be on the left side and always there -->
<a href="#{r(:index)}">Home</a> |
<a href="#{r(:about)}">About Us</a> |
<a href="#{r(:help)}">Help</a>
<?r else ?>
<!-- Admin controller -->
<!-- Move this next section over to the right side of the screen. It
will contain the Login/Register if we're not logged in and the Logout
if we are. -->

<span style="float: right">
<!-- We're going to use the private method logged_in? here to test if
we want to show the Login/Register links or the Logout link -->

<?r if !logged_in? ?>
<a href="#{r(:login)}">Login</a>
<?r else ?>
<a href="#{r(:logout)}">Logout</a>
<?r end ?>
</span>

<!-- These next three will be on the left side and always there -->
<a href="#{r(:main)}">Home</a>
<?r end ?>
</div>

<div id="content">
<!-- Display the actual content. This will come from the method or the
associated view/*.xhtml file
-->

#@content
</div>

<!-- Set the footer in the center of the screen. -->
<div id="footer" style="text-align: center;">
<h5> Powered by Ramaze </h5>
</div>
</div>
</body>
</html>



We've reused this a number of times, so it should look pretty familiar. In the head section, we set up for our JavaScript including for jQuery. This time around, we grab it from Google as per current best practices (possibly the only best practice here). Then we have the JavaScript for our particular page (this being whatever page the user is on that needs JavaScript. Next is the body where we have the "header" (which based on an interesting book I'm reading right now, Transcending CSS by Andy Clark, I'd probably relabel as "branding") with our title. Next is the "nav" section with two parts, one for the admin and one for a normal user. This is followed by the "content" which is really whatever is filled in by each of our methods in the controller and finally we have the footer (once again probably renamed to something like "siteinfo").

Finally, here's our CSS (once again, nothing we haven't seen before).


# public/page.css
#whole_page {
width: 50em;
margin: auto;
padding: 0;
text-align: left;
border-width: 0 1px 1px 1px;
border-color: black;
border-style: solid;
}

#content {
height: 100%;
background: white;
padding: 1em 1em 1em 1em;
}


/* Header CSS */
#header {
background:#9DA9EE;
color: white;
margin-bottom: 0;
padding: 0.25em;
}

#nav {
background:#9DA9EE;
color: black;
padding: 0.5em;
}


/* Footer CSS */
#footer {
background:#9DA9EE;
color: black;
}


So, I think that's everything (let me know if I missed anything). This pretty much started out as an example for using mail, but that really proved to be the least interesting part of this given how easy it is to use the Mailit gem.

Let me know if you have any questions or comments and I'll do my best to answer them.

Wednesday, October 7, 2009

Sequel Models many_to_one / one_to_many (Revisited)

I was working on a post to show how to user Ramaze and Sequel to issue challenge questions if a user forgets his password. I was using a one_to_many / many_to_one relationship between users and challenge questions (each user will have a single challenge question and each challenge question could have many users). In the past when I've used this relationship, I've added the one to the many using the add_X() method. For example in some library code I was writing I had the concept of locations that could have copies of books. Each location could have many copies, but each copy would have only a single location. I would use something along the lines of location.add_copy(copy) when I created a new copy of a book. In the user/challenge question though it made more sense (to me anyway) to add the challenge question to the user. I first tried user.add_challenge_question(challenge_question) and that failed. I finally emailed the Sequel list and when you're adding that direction, you need to just use an "=". So you end up with user.challenge_question = challenge_question.

The other issue I had was the naming of the challenge question model. In this case, I had named the database table "challenge_questions". I thought then that the model should be Challenge_Question, but apparently it should be ChallengeQuestion. I vaguely recall seeing this somewhere (Rails perhaps?), but thought I'd get it written down here and hopefully save someone else the trouble.

Here's some sample code:

require 'rubygems'
require 'sequel'

# Create an in-memory database
DB = Sequel.sqlite

# Create the users table that will contain a user name and
# a foreign key to the challenge question for this user.
DB.create_table(:users) do
primary_key :id
String :user_name
foreign_key :challenge_question_id, :challenge_questions
end

# Create a table for the challenge questions.
DB.create_table(:challenge_questions) do
primary_key :id
String :question
end

# Create the User model. This is many to one with the
# challenge_questions table. So ... many users can have
# one challenge_question. Note: challenge_question is
# singular.
class User < Sequel::Model
many_to_one :challenge_question
end

# Create the Challenge Question model. This is one to many
# with the users table. So ... one challenge_question can have
# many users. Note: users is plural. Also note that even though
# the table had an "_" (underscore), the model does not.
class ChallengeQuestion < Sequel::Model
one_to_many :users
end

# First we'll create a user and a challenge question then
# add the challenge question to the user. Since user is many to one
# with the question, we can just use the "=" (equal) sign.
u1 = User.create(:user_name => 'A User')
q1 = ChallengeQuestion.create(:question => 'Where?')
u1.challenge_question = q1
puts "User: #{u1.user_name} Question: #{u1.challenge_question.question}"

# First we'll create a user and a challenge question then
# add the user to the challenge question . Since challenge_question is one to many`
# with the user, we use the add_user() method.
u2 = User.create(:user_name => 'Another User')
q2 = ChallengeQuestion.create(:question => 'Who?')
q2.add_user(u2)
puts "User: #{u2.user_name} Question: #{u2.challenge_question.question}"

# Add another user and show that we can use the add_user() method to
# add them to the second question.
u3 = User.create(:user_name => 'Yet Another User')
q2.add_user(u3)
puts "User: #{u3.user_name} Question: #{u3.challenge_question.question}"


The code explains pretty well what's going on and with the notes above, you should be fine.

Let me know if you have any questions or comments.

Wednesday, September 16, 2009

Ramaze, Full Calendar, JSON, and AJAX

Where I work we have numerous environments for different customers and each of these environments can have different versions of our software running depending on where the customers are in terms of their release cycles. It became apparent a while back that what we really needed was a calendar to plan the deployments to these different environments. Since we also wanted multiple views, I decided to look into writing a little Ramaze application that would handle this task. I'm not going to show the entire application here, but will show how to use the jQuery calendar I selected, FullCalendar (http://arshaw.com/fullcalendar/) and how to use the Ruby JSON gem.

First thing is to get the new code you'll need for this project. First off, grab the JSON gem. Type:

sudo gem install json_pure

This will install a pure ruby implementation of the gem. You can go here to find out more about the gem and how to use it.

Next, you'll have to download the FullCalendar. This will give you the JavaScript and CSS you'll need for the calendar.

OK, so let's create the directory structure for this project. You'll need a top level directory, called say FullCalendarTest. Under that you'll need controllers, layout, public, and view directories. Under the public, you'll need a js directory and that's pretty much it. We're not going to use a database here, so you won't need our normal dbMigration or models directories.

Let's get the calendar code and CSS into the correct spots for this demo. From the directory you unzipped your FullCalendar, copy ui_core.js, ui_draggable.js, and fullcalendar.js to the public/js directory. You should also copy the jquery-1.3.2.js there also. Finally, copy from the same FullCalendar unzipped directory the fullcalendar.css file to the public directory. With that, we should be all set up to start writing our own code.

First up is our start.rb file.


# start.rb
#
# This is the main program for the example. It loads the loads the controller
# and then starts up Ramaze.
#
require 'rubygems'
require 'ramaze'
require 'json'

require 'controllers/main_controller'

Ramaze.start :port => 7001


Nothing too much here different that what we've done before. We aren't using a database here so there's none of the normal sequel code that we've seen before (add it back in if you decide to flesh this example out). The only other thing is the Ramaze.start :port => 7001. Normally, we wouldn't put a port number on this, but I was running my original application while developing the demo and put this in so I could run both. Normally, the default port for Ramaze is 7000 and we don't specify it. Here, where we do want a different port, we'll put it in. In a "real" web application, you'd probably put in the normal http port of 80.

Let's take a look at our controller, controllers/main_controller.rb next.


# controllers/main_controller.rb
#
# This example shows how to do use the FullCalendar (http://arshaw.com/fullcalendar/) and AJAX.
class MainController < Ramaze::Controller
# The controller will be accessed using "/" as in:
# http://localhost:7001/.
map '/'

# Layout using page but not if it comes from an AJAX request.
layout(:page){ !request.xhr? }

helper(:xhtml)

# You can access it now with http://localhost:7001/
def index
# Set the title for the page.
@title = "Ramze Calendar Test"

# Set the javascript for this page. In this case it's the script to
# set up and display the calendar in public/js/show_calendar.js.
@page_javascript = 'show_calendar'

if request.xhr? # came from ajax request

# Here's how to get the dates that will be sent by FullCalendar. We're not actually going
# to use them here, but this will show what to do when you actually need them.
startDate = Time.at(request['start'].to_i).strftime("%Y-%m-%d")
endDate = Time.at(request['end'].to_i).strftime("%Y-%m-%d")
Ramaze::Log.debug("show_calendar: Have an Ajax request start: #{startDate} end: #{endDate}")

# Use the JSON gem to generate JSON for the events. We're just going to add a
# couple of events here. One will be on the 15th of September 2009 and the other
# on the 17th. Normally, these would come out of the database and the "id" would be
# their id in a database table, the "start" would a date from a table in the database. Finally,
# the url would be a link to a page where you could chang the event and then save it. Of course,
# you don't have to do it that way, but it would be one way to generate the events.
json = JSON.generate [
{"id"=>1, "title" => "Ramaze", "start" => "2009-09-15", "url" => "http://ramaze.net/"},
{"id"=>2, "title" => "Sequel", "start" => "2009-09-17", "url" => "http://sequel.rubyforge.org/"}
]

# It looks like we a) MUST use the respond command and b)MUST use the 200 return value. This was
# determined by just trying different things.
respond(json, 200)
end
end
end


This is actually much smaller that it looks due to the excessive amounts of commenting in it. We have our normal "startup" code with the map, layout, and helper lines. The layout(:page){ !request.xhr? } just makes sure that we don't use the layout when we're handling an AJAX request. We've seen this before in our previous AJAX tutorial. Next up, we have the index method (our only one). This will set the title and then the JavaScript for the page, in this case it will end up being public/js/show_calendar.js (the actual script tag will get created in the layout). Next we put the code for handling an AJAX request. The calendar is going to pass us start and end dates in the request hash table with the keys of "start" and "end" appropriately enough. Here, we're not going to actually use them, but I've shown how to parse them out for when you do actually want to go to a database to get some actual events. After this, we create a json structure from an array of hashes. Here we create a couple of events for the 15th and 17th of September, 2009. These are hard coded, so feel free to change them if you'd like. Finally, we send the json back to the FullCalendar code. In our previous AJAX example, we just passed the json back directly. For whatever reason, that doesn't work with the FullCalendar code and we use the respond(json, 200) to send it back. This could be because the FullCalendar code uses the getJSON() call rather than the post(). I haven't investigated this, but if you do, let me know what you find in the comments section.

Let's take a quick look at the layout in layout/page.xhtml


<!-- view/page.xhtml -->

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

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<!-- Use the page.css in the public directory and set title based on
what's set in the associated method.
-->

<link rel="stylesheet" type="text/css" href="/page.css"/>
<link rel='stylesheet' type='text/css' href='/fullcalendar.css' />

<!-- Our jQuery is in the public/js directory -->
<script type="text/javascript" src="/js/jquery-1.3.2.js" ></script>
<script type='text/javascript' src='/js/jquery/ui.core.js'></script>
<script type='text/javascript' src='/js/jquery/ui.draggable.js'></script>
<script type='text/javascript' src='/js/fullcalendar.js'></script>

#{ js @page_javascript }

<title>#@title</title>
</head>

<body>

<div id="header">
<h1>Ramaze Calendar Test</h1>
</div>

<!-- Display the actual content. This will come from the method or the
associated view/*.xhtml file
-->

#@content

<!-- Set the footer in the center of the screen. -->
<div id="footer" style="text-align: center;">
<h5> Powered by Ramaze </h5>
</div>
</body>
</html>


We include the two style sheets for our normal page.css and also for the calendar, fullcalendar.css. Next we have our JavaScript code for jQuery and and the FullCalendar followed by the page_javascript. In this case the only page JavaScript we'll have is the public/js/show_calendar.js. Everything else, we've seen before.

Here's the show_calendar.js (formatter doesn't work on javascript code, sorry)


/* public/js/show_calendar.js */

$(document).ready(function() {

$('#calendar').fullCalendar({
/* The draggable looks like it might be pretty cool, but when I try to enable it
* only the first event will be shown. Go ahead and give it a try and if you find
* a solution, let me know.
*/
/* draggable: true, */

/* The events will use the "index" method in the main controller to get the events from. */
events: "/index",

/* We're not using drag/drop here (see above), but it would be nice to have it. */
eventDrop: function(event, delta) {
alert(event.title + ' was moved ' + delta + ' days\n' +
'(should probably update your database)');
},

/* What to do while we're loading. */
loading: function(bool) {
if (bool) $('#loading').show();
else $('#loading').hide();
}
});
});



This starts out like all jQuery functions with the ready() function. Next, we have the commented out draggable: true which when I left it in, would only show the first event. This was true whether I put events in-line or got them from AJAX. Then we have the events: "/index", line which tells FullCalendar to get the events from an AJAX call to our index method in the main_controller. Finally, we have a couple of items for dragging (not used) and loading.

Here's the view/index.xhtml:

<div id='calendar'></div>



OK, the only thing in here is the calendar itself.

Our page.css is also very simple and is just like what we've used numerous times before.


/* Header CSS */
#header {
background:#9DA9EE;
color: white;
margin-bottom: 0;
padding: 1.5em;
}

/* Footer CSS */
#footer {
background:#9DA9EE;
color: black;
}

/* Calendar */
#calendar {
width: 900px;
margin: 0 auto;
}


The only addition is for the calendar.

Everything else used is from FullCalendar itself and you can check the documentation for it here.

I think that's about everything. FullCalendar works well for what I'm going to be using it for, although it would be nice to have the drag and drop interface working. If I get the issues with that worked out, I'll either edit the post or create another show post on how I solved it. If you do end up trying to use some of this with a database and sequel and have problems, let me know and I'll be glad to post some code on how I've managed it.

One final thing is the help I received from the Ramaze mailing list on this. First, thanks to hrnt for the hint on respond and to Greg for telling me how to view AJAX repsonses in Firebug. The thread is here.

Let me know if I've missed anything or if you have questions.

Friday, August 7, 2009

Ramaze, Sequel, and Search (Round 2)

After my last post, Jeremy Evans had a suggestion that I felt was worth passing on. He points out that you probably shouldn't be using datasets in models and views and instead should move them to the models and let the model handle it. So ... here's the new controller controllers/main_controller.rb

# controllers/main_controller.rb
#
# The mainController has a two methods index and search_results. First map to /
# for the view. Next, set the layout to page so that we use the
# layout/page.xhtml for our layout.
class MainController < Ramaze::Controller

# The Admin controller will be accessed using "admin" as in:
# http://localhost:7000/admin.
map '/'

# Use page.xhtml in the layout directory for layout
layout :page

# You can access it now with http://localhost:7000/
def index
@title = "Search Example"
end

# Calculate and display the search results.
def search_results
@title = "Search Example - Results"

# Grab the search_string from the request hash. This is generated when the user inputs
# something into the Search box in the view/index.xhtml file.
@search_string = request[:search]
@search_response = Book.search(@search_string.split)
end
end



and the new models/models.rb

#
# This is the model for the book and is backed by the :book table in the
# database.
#
# Create the Book model.
class Book < Sequel::Model
def self.search(terms)
Book.grep([:title, :description], terms.map{|x| "%#{x} %"}).all
end
end


Not too big a change, but it does make things a bit easier to use and test.

As always, let me know if you have questions.

Ramaze, Sequel, and Search

If you've been following my emails on the Ramaze and Sequel lists, you've probably realized that I've been working on a Ramaze application for a library. This project came about when our company decided that we needed a library and we started looking for software. There wasn't anything that really met our needs so I decided to take a stab at writing it myself. It's a pretty good sized application now and still not "finished", but I thought in these next few posts, I'd lay out some of the things that I've learned while working on it in smaller pieces than the whole application.

The first thing I'd like to show is the search function. This is much more simple than I thought it would be. What we'll do is create some books with titles and descriptions. We'll add a page that allows the user to input some search terms and then another page that displays these results. The results will be based on whether any of the search terms are found in either the title or the description.

First let's create the database with our migration. Here's the code for this. Note that we're adding the data as well as creating the database so we don't have to create the data using another page.

# dbMigration/001_SearchMigration.rb
#
# This is the "new" way to do migrations by using the Class.new form.
# It means that a new class name is not required and so there is no chance
# of creating a second class in the migration files that is the same as the
# first causing problems.
Class.new(Sequel::Migration) do
def up

create_table(:books) do
primary_key :id
String :title
String :description
end

from(:books).insert(:title => 'Programming Ruby', :description => 'A great Ruby book')
from(:books).insert(:title => 'Agile Web Developement with Rails', :description => 'A book about Ruby on Rails')
from(:books).insert(:title => 'Cryptonomicon', :description => 'A book about a Unix sys admin')
from(:books).insert(:title => 'The C Programming Language', :description => 'A book about programming in C')

end

def down
drop_table(:books)
end
end


We've seen this before. We create the table called "books" (note the plural) with text columns for the title and the description. Our down method, just drops the table. In the middle we add four books with their titles and descriptions.

Next let's look at our models. Jeremy Evans, the Sequel maintainer/guru, recommends having a single models.rb in our models directory to make it easier to use irb to test things. I've taken to doing this and it works quite well. Here our model is very simple (empty). We name the model Book (singular of the table books) and derive it from Sequel::Model. This will give us access to books table. Here's the actual code:

#
# This is the model for the book and is backed by the :book table in the
# database.
#
# Create the Book model.
class Book < Sequel::Model
end


The controller, controllers/main_controller.rb is also quite simple. It has an index method that only sets the title and a second method search_results that "calculate" the results and save them to the @search_response variable for use in the view/search.xhtml view. We also go ahead and save the @search_string so we can display that we can display that also. The last line, that calculates the search_response, probably needs a bit of explanation.

We're going use the "grep" method on the Book dataset. We will pass an array with :title and :description to let the method know which columns of Book we're interested in. The next piece, we take the search_string and split it into an array. We then "map" the array generating something that will look like:

%ruby % %rails %

for a search string of "ruby rails". You can read about the grep function in a Sequel dataset here.

Here's the controller code:

# controllers/main_controller.rb
#
# The mainController has a two methods index and search_results. First map to /
# for the view. Next, set the layout to page so that we use the
# layout/page.xhtml for our layout.
class MainController < Ramaze::Controller

# The Admin controller will be accessed using "admin" as in:
# http://localhost:7000/admin.
map '/'

# Use page.xhtml in the layout directory for layout
layout :page

# You can access it now with http://localhost:7000/
def index
@title = "Search Example"
end

# Calculate and display the search results.
def search_results
@title = "Search Example - Results"

# Grab the search_string from the request hash. This is generated when the user inputs
# something into the Search box in the view/index.xhtml file.
@search_string = request[:search]
@search_response = Book.grep([:title, :description], @search_string.split.map{|x| "%#{x} %"}).all
end
end


The search.xhtml contains the form to type in the search term(s) and submit it. On the form we use the action attribute to send the results to the search_results method in the controller. We use a "get" method so the results are passed on the URL (allowing bookmarking as Gavin pointed out when I asked how to do this incorrectly in a Ramaze thread. I had he and Clive steer me in the right direction though). The URL will look something like http://localhost:7000/search_results?search=ruby+rails when you're searching type "ruby rails" in the text box. Here's the view/index.xhtml:

<!-- view/index.xhtml -->
<!-- Create the form for the search. We're going to set the action to
search_results so that method will get called when the form is
submitted and we'll use a "get" method to pass the parameters in
the URL.
-->

<form id="search" action="search_results" method="get">
<fieldset>
<legend> Search </legend>
<div>
<!-- for= goes with id=, the name= is placed in the request variable. -->

<!-- Input for the title. -->
<label for="search">Search:</label>
<input id="search" name="search" type="text" />
<br/>

<!-- Submit the edited book values. -->
<input type="submit" value="Search" />
</div>
</fieldset>
</form>


The page to display the search response, view/search_results.xhtml, is also pretty simple. It takes the results saved in @search_response by the search_results method in the controller, loops through them and displays each of the books and their descriptions. Here's the code:

#{flashbox}
<br/>
<?r if @search_response.each && @search_response.size > 0 ?>
Results found for "#{@search_string}." <br/><br/>
<?r @search_response.each do | book | ?>
Title: #{book.title}
<br/>
Description: #{book.description}
<br/><br/>
<?r end ?>
<?r else ?>
No results found for "#{@search_string}".
<?r end ?>
<br/>


Finally, here's the layout (layout/page.xhtml):

<!-- view/page.xhtml -->

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

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<!-- Use the page.css in the public directory and set title based on
what's set in the associated method.
-->

<link rel="stylesheet" type="text/css" href="/page.css"/>

<title>#@title</title>
</head>
<body>
<div id="header">
<h1>Search Example</h1>
</div>

<div id="content">
<!-- Display the actual content. This will come from the method or the
associated view/*.xhtml file
-->

#@content
</div>

<!-- Set the footer in the center of the screen. -->
<div id="footer" style="text-align: center;">
<h5> Powered by Ramaze </h5>
</div>
</body>
</html>


and the CSS file (not formatted):

/* Header CSS */
#header {
background:#9DA9EE;
color: white;
margin-bottom: 0;
padding: 0.25em;
}

/* Footer CSS */
#footer {
background:#9DA9EE;
color: black;
}

All told, pretty simple and easy after someone points you in the right direction anyway.

As always, let me know if you have questions in the comments and I'll do my best to answer them.

Wednesday, July 8, 2009

Creating a Poll with Ramaze and Sequel; Updated 2009-07-10

Jeremy Evans of Sequel had a few comments on the code:


  • add the :polls here: foreign_key :poll_id, :polls in the migration.

  • change :questions to :responses in the drop in the migration.
  • make the drop a single line since it will take multiple values and drop the :responses first as in drop_table(:responses, :polls)

  • don't use the -M on the sequel migration as going to the latest is the default as in
    sequel -m dbMigration/ sqlite://polls.db

  • load the models before the controllers in start.rb as the controllers will depend on the models

  • remove the requires in the models as they can't be loaded without the database anyway.



I've updated the code below to reflect this.

Because of their great accuracy, Internet based on-line polls are a popular way to gather information. OK, if HTML supported sarcasm tags, that previous sentence would have to be enclosed in them. Still a lot of sites feature polls and they can be fun even if they aren't particularly useful or reliable. We'll use Ramaze and Sequel here to create a very simple on-line polling system. With the proper enhancements (say putting a login/password on the admin page), you could use this in your site.

We're going to create a "Poll of the Day" site that allows an administrator to create a poll for a given day and give it a title, the question, and some responses. These will be saved in the database. The main poll site will allow a user to answer the poll question for the day and then will be redirected to the results page to see the current results. The user can also go directly to the results page if desired.

Let's start with the database. Here's the Sequel migration for it:


# dbMigration/001_PollMigration.rb
#
# This is the "new" way to do migrations by using the Class.new form.
# It means that a new class name is not required and so there is no chance
# of creating a second class in the migration files that is the same as the
# first causing problems.
Class.new(Sequel::Migration) do
def up
create_table(:polls) do
primary_key :id
String :title
String :question
Date :date
end

create_table(:responses) do
primary_key :id
String :response
Integer :count
foreign_key :poll_id, :polls
end
end

def down
drop_table(:responses, :polls)
end
end



Here we're going to create two tables, one for the poll and one for the responses to the poll. The first will contain a title, a question, and a date. The second a reponse and a count of the number of times that response was selected. There will be a one-to-many / many-to-one relationship between the polls and the responses. In other words, each poll can have many responses, but each response will have only a single poll.

To run this use:
sequel -m dbMigration/ sqlite://polls.db

This should create the two tables with their associated columns. You can check this using the sqlite manager for Firefox that's available here.

Once we've created the database, we can start on the actual code. Let's start with start.rb our main program.



# start.rb
#
# This is the main program for the example. It loads the Sequel database,
# loads the controllers and models, and then starts up Ramaze.
#
# The database should have been set up using the database migrations in
# the dbMigration directory.
require 'rubygems'
require 'ramaze'
require 'sequel'

# Open the polls database. This must be done before we access the models
# that use it.
DB = Sequel.sqlite("polls.db")

# Load the controllers and models.
require 'models/poll'
require 'models/response'
require 'controllers/main_controller'
require 'controllers/admin_controller'

Ramaze.start


There's nothing in here that's different from what we've done in previous examples. We load the database, load the controllers, load the models and finally start up Ramaze.

The models are also very simple. Here's the models/poll.rb:



# models/poll.rb
#
# This is the model for the Poll and is backed by the :polls table in the
# database.
#
# Create the Poll model.
class Poll < Sequel::Model
one_to_many :responses
end


There's nothing too much in here. We, as is usual derive from Sequel::Model and then note the one_to_many relationship with the responses table.


# models/response.rb
#
# This is the model for the Response and is backed by the :responses table in the
# database.
#
# Create the Response model.
class Response < Sequel::Model
many_to_one :polls
end



Also, nothing much. We derive from Sequel::Model and then have a many_to_one relationship with the polls table.

Next, let's look at the controllers. First we have the admin controller in controllers/admin.rb:

# controllers/admin_controller.rb
#
# The AdminController has a single method index. First map to /admin for
# the view. Next, set the layout to page so that we use the layout/page.xhtml for our
# layout. Then we use a helper for the :xhtml which will allow us to use "js" in the
# layout (we use this to generate a javascript link).
class AdminController < Ramaze::Controller
# The Admin controller will be accessed using "admin" as in:
# http://localhost:7000/admin.
map '/admin'

# Use page.xhtml in the layout directory for layout
layout :page

helper(:xhtml)

# You can access it now with http://localhost:7000/admin
def index
@title = "Poll of the Day - Administration"
@page_javascript = "admin"

# If this was from a post, we grab the information out of the request hash
# and create a new Poll and however many we receive responses. We add the responses
# to the poll, set the flash message, and then stay on this page in case the
# user wants to add more polls.
if request.post?
title = request[:title]
date = request[:date]
question = request[:question]
poll = Poll.create(:title => title, :date => date, :question => question)
responses = request[:response]
responses.each do | r |
poll.add_response(Response.create(:response => r, :count => 0))
end
flash[:message] = "New poll accepted"
redirect rs(:index)
end
end
end



Once again, everything here we've mostly seen before. The only "interesting" piece is the responses that we grab from an array (we'll see how to set that up when we look at the view). Basically, we grab the data from the request hash, create a Poll using said data, and then loop through the response array creating Responses and adding them to the Poll. Just a note, there's no need to pull out the title, question, and date separately. I just did that so that I could debug a bit easier while I was developing. Feel free to just put the request[] into the Poll.create code. After the poll and responses are created, we just set the flash message to let the user know the poll was created and then just redirect to the same page so they can enter another poll if desired.

Now, here's the view, view/admin/index.xhtml


#{flashbox}
<h2>Poll of the Day - Administration</h2>

<form id="new_poll" method="post">
<fieldset>
<legend> Poll Information </legend>
<div>
<!-- for= goes with id=, the name= is placed in the request variable. -->

<!-- Input for the title, question, and date. -->
<label for="title">Title:</label>
<input id="title" name="title" type="text" />
<br/>
<label for="question">Question:</label>
<input id="question" name="question" type="text" />
<br/>
<label for="date">Date(yyyy-mm-dd):</label>
<input id="date" name="date" type="text" />
<br/>
<!-- This div, responses, is to add new input text boxes for responses -->
<div id="responses">
<!-- We're going to use the square brackets ([]) to let Rack (I believe)
know that we want these to come across the in the request hash as
an array.
-->

<label for="response[]">Response:</label>
<input id="response[]" name="response[]" type="text" />
</div>
<br/>
<!-- Submit the new poll (this should result in it being saved in the database -->
<input type="submit" value="Submit New Poll" />
</div>
</fieldset>
</form>



As you can see, the first part of this is pretty straightforward. We simply put input text boxes for the title, question and date of the poll. Then we have a div for the responses followed by the response text box. Note that the label on the text box contains square brackets on it. This tells Rack (I believe it's not Ramaze) that we want to return this as an array and not as a single value. We need to take a look at the JavaScript to see how additional response text boxes get added.

$(document).ready(function() {
// For the responses div, find any input and if we hit a return (13), then
// clone the input text box and add a new input text box after this one.
$("#responses input").keypress(function(e) {
// If we receive an "Enter" instead of submitting
// the form, clone the input and return false to
// stop the submit.
if (e.which == 13) {
// We have and Enter, so clone this input text box and
// insert the new one after this one.
$(this).clone(true).insertAfter(this).val("hello");

// Return false so we don't do the submit of
// the form we'd normally do here.
return false;
}
});
});

This is JavaScript file contains some jQuery code to add a function all input tags under the responses div id. This function will look for the Enter key (value 13) to be hit and when it is will clone the current text box and add the new text box after itself. In this way, the user can add more responses without being limited to a set number. This function returns false so that when the Enter is hit, the form is not submitted as is normal. To submit the form, the user need to actually click the "Submit New Poll" button.

That's pretty much it on the admin side, let's take a look at the end user side. Here we have the main controller

# controllers/main_controller.rb
#
# This example shows how to do a simple poll web page. It has two
# methods (index and results). The index method will display the current day's
# poll and allow the user to vote. When the user does vote, their vote will be
# counted, saved, and they will be redirected to the results page. The results
# page will display the current results for today's poll. There is one private
# method that both methods call to get the Poll for today. It will try to find the
# poll based on today's date and if it can't find it, will return nil. If nil is
# returned to either method, it will set the flash value to "No poll ...".
class MainController < Ramaze::Controller
# Use page.xhtml in the layout directory for layout
layout :page

helper(:xhtml)

# You can access it now with http://localhost:7000/
def index
# Set the title for the page.
@title = "Poll of the Day"

# Get today's poll and set the flash message
# if there's not one.
if !(@today_poll = todays_poll)
flash[:message] = "No poll for today"
end

# If the user voted, it should come in a a post. The
# request[:choices] represents the value that was on the
# radio button in view/index.xhtml. We'll use that to get
# the Response from models/response.rb. We increment the
# count in the response and then save the new value. Finally,
# we'll redirect to the results page to show the user the
# current vote count.
if request.post?
response = Response[request[:choices]]
response[:count] = response[:count] + 1
response.save
redirect r(:results)
end
end

# You can access it now with http://localhost:7000/results
# This will show the user the results for the voting on the
# current poll.
def results
# Set the title for the page.
@title = "Poll of the Day - Results"

# Get today's poll and set the flash message
# if there's not one.
if !(@today_poll = todays_poll)
flash[:message] = "No poll for today"
end
end

private

# Get today's poll from the database using today's date to find it. This method
# is private so it can be used by the two methods above, but can't be reached
# from the outside.
def todays_poll
Poll.find(:date => Date.new(Time.now.year,Time.now.month,Time.now.day))
end
end



First up we have the layout and the xhtml helper discussed in the admin controller. Next we have the index which will get displayed if you don't request anything else (i.e. go straight to http://localhost:7000/). Since this is the default, we don't need a map command as we did in the admin controller. We set the title (as always), then check get today's poll (@todays_poll) using the todays_poll method (sorry for the confusing naming). If there is no poll, we simply set the flash message which will get displayed in the view. Next, if this was a post (someone selected something from the radio boxes in the view), we increment the count in the response database, save the count, and then redirect to the results page.

Next up we have the results method which is available directly at http://localhost:7000/results or when you "vote" on the main page. Here all we do is set the title, get today's poll or set the message if it is not there.

Finally, there's the todays_poll private method. It's private so you can't get to it via a URL. It simply returns today's poll by finding a poll by creating a date from the current date. It would be much cleaner if there were a Date.now method to match the one in Time.

Here's the view for index:


<!-- view/index.xhtml -->
<h2>Today's Poll</h2>
#{flashbox}
<!-- Main page. Display the title, the question, and then radio boxes
for the choices. When they select, it will be submitted back to
the index page.
-->

<?r if @today_poll ?>
<h3>#{@today_poll.title}</h3>
<h3>#{@today_poll.question}</h3>
<form id="new_poll" method="post">
<!-- for each of the possible responses in today's poll, create a
radio button with the id (this is the id in the Response database
table) as the value and the response
as the text
-->

<?r @today_poll.responses.each do |r| ?>
<input type="radio" name="choices" value=#{r.id}> #{r.response}<br>
<?r end ?>
<!-- Submit the user's choice in the poll -->
<input type="submit" value="Vote" />
</form>
<?r end ?>



Nothing too special in here. If there's a poll for today, we put the title and question using the @todays_poll value and then loop on the responses creating a radio button for each of them. Finally, there's a submit button labeled "Vote".

Here's the results view:


<!-- view/results.xhtml -->
<h2>Today's Poll Results</h2>
#{flashbox}
<!-- Results page. Using today_poll, Display the title, the question, and then
the reponses and their counts.
-->

<?r if @today_poll ?>
<h3>#{@today_poll.title}</h3>
<h3>#{@today_poll.question}</h3>
<?r @today_poll.responses.each do |r| ?>
#{r.response} : #{r.count}<br>
<?r end ?>
<?r end ?>



We simply check if there is a poll and if there is display the title, question, and then the counts for each of the reponses.

As always, let me know if you have any questions or comments.