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:
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.
require 'rubygems'
require 'ramaze'
require 'sequel'
DB = Sequel.sqlite("polls.db")
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:
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.
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:
class AdminController < Ramaze::Controller
map '/admin'
layout :page
helper(:xhtml)
def index
@title = "Poll of the Day - Administration"
@page_javascript = "admin"
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>
<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/>
<div id="responses">
<label for="response[]">Response:</label>
<input id="response[]" name="response[]" type="text" />
</div>
<br/>
<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
class MainController < Ramaze::Controller
layout :page
helper(:xhtml)
def index
@title = "Poll of the Day"
if !(@today_poll = todays_poll)
flash[:message] = "No poll for today"
end
if request.post?
response = Response[request[:choices]]
response[:count] = response[:count] + 1
response.save
redirect r(:results)
end
end
def results
@title = "Poll of the Day - Results"
if !(@today_poll = todays_poll)
flash[:message] = "No poll for today"
end
end
private
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:
<h2>Today's Poll</h2>
#{flashbox}
<?r if @today_poll ?>
<h3>#{@today_poll.title}</h3>
<h3>#{@today_poll.question}</h3>
<form id="new_poll" method="post">
<?r @today_poll.responses.each do |r| ?>
<input type="radio" name="choices" value=#{r.id}> #{r.response}<br>
<?r end ?>
<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:
<h2>Today's Poll Results</h2>
#{flashbox}
<?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.
Hello,
ReplyDeletesorry I am a total Ramaze newbie.. I have problems running your example. The admin page as well as the index page stay empty even if there are entries in the table. I wonder how @today_poll is defined - I see that it is checked for in some places, but I can't find the place where it receives any value..
heko, @today_poll is defined in this statement:
ReplyDeleteif !(@today_poll = todays_poll)
flash[:message] = "No poll for today"
end
and one other just like it. Note that it's an "=" and not an "==". Really, I probably shouldn't use an assignment in an "if" like that.
Now, that being said, I'm not sure why you're not seeing anything. It may be something got messed up in formatting. If you let me know your email, I'll zip up the code and send it to you and maybe that will help. Also are you on Windows, Linux, or Mac. It should work on all as far as I know, but I've never run it on anything but Linux.