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.