
Perl Case Study - Poll CGI
By Lisa Hui
Polls are a cool application that's both interactive and probably informative.
I've seen and created some, ranging from simple to difficult, but first thing's
first: you have to be able to understand how they work and create the basic
before moving on to the difficult applications. We're going to start with
a simple application and build up from there. Error checking is minimal until
we get into the advanced parts.
Planning the Script:
Purpose: To add real-time interactivity to a website and provide interesting
statistics for visitors to view.
Interaction Features: The user should be able to see the poll and be given
a chance to add a vote to the choice that best fits. Once the vote is submitted,
he or she should not be able to vote again and be shown the current results.
(Optionally, the voter can be given the privilege of viewing the current
results without voting).
-
Displaying the poll question and choices to vote on
-
Displaying the current poll results
-
Receiving the data (user requesting a certain action - such as displaying
data and voting - to be performed)
-
Interpreting the data/commands
Structure:
How the script will do what it does:
A command will be used to tell the script what the user wants to do or see.
We will have the following command options: vote and results.
If there is a need to
Receive command and data
Interpret command and parse data
Modify the poll's data files as necessary
Display a HTML page created on-the-fly based on command
Extra Script Features:
-
Web based modification of poll topic and questions
-
Choice user-submitted comments and reports
-
Results with graphical bar display
Giving Commands
The easiest way for a person to access a poll is probably to go to an HTML
page given by a certain URL, probably something like
http://www.yoursite.com/poll/vote.html and
http://www.yoursite.com/poll/results.html.
But we can get our output directly (and in real-time) from the CGI script
itself. The URL would probably something look like:
http://www.yoursite.com/poll.cgi. You already know that you can tell
a CGI script what to do via a form, but there's yet another alternative to
pass this "command" information straight to the script via its URL (more
specifically, though its query string). If you wanted to directly
access the voting page (the HTML equivalent would be vote.html in
our example) we might want to just type in something like
http://www.yoursite.com/poll.cgi?show=vote. That tells the
script that you want to see the voting page. Likewise,
http://www.yoursite.com/poll.cgi?show=results would tell the
script that you would want to see the results.
This isn't a built in Perl structure! But it does take advantage of
the parse/get data routine that was detailed in our
Form CGI Case Study. How does the structure?
Everything after the question mark (?) is given to the CGI script in the
environment variable $ENV{'QUERY_STRING'}. By default, anything passed through
the URL to give the CGI script information uses the GET method (discussed
in our HTML forms compendium). The
form parsing routine takes a pair, which in our example is "show=vote." The
first, "show," becomes a key in the hash %FORM (see
Perl Basics: Becoming a Perl Newbie
Extraordinaire to find out more about hashes). The second, "vote," is
the value stored in $FORM{'show'}. In other words, that entire query string
to poll.cgi resulted in the creation of a key/value pair creation: the equivalent
of saying $FORM{'show'} = "vote";
Thus, it is possible to specify any key/value pair you wish. Instead of show,
we could have said http://www.yoursite.com/poll.cgi?page=vote. The
only difference in this case is that the key is different. It is the equivalent
of declaring $FORM{'page'} = "vote"; (Similarly, the "vote" value
can be whatever you want it to be - just keep track of what it is and what
you want the script to do if the "page" value is "vote" if you get my drift.)
If you don't have a clue as to how we are going to make the script perform
actions based on the value of $FORM{'show'} (we will be using "show" as the
key value in the hash) check out the Mailing
List CGI Case Study.
Okay, now that we're set with how URLs can pass data to a CGI script and
you're already aquainted with how forms pass data to a CGI script, we're
going to take the two cases: vote and results. We start out with the basics
of the script:
#!/usr/local/bin/perl
######################
# File: poll.cgi
print "Content-type: text/html\n\n";
###### Get and Parse Data ######
if ($ENV{"REQUEST_METHOD"} eq 'GET') { $buffer = $ENV{'QUERY_STRING'}; }
else { read(STDIN, $buffer, $ENV{'CONTENT_LENGTH'}); }
@pairs = split(/&/, $buffer);
foreach $pair (@pairs) {
($name, $value) = split(/=/, $pair);
$value =~ tr/+/ /;
$value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;
$FORM{$name} = $value;
}
#Decide what to do based on the value of the $FORM{'show'} variable
#Three functions:
#1st to show the voting page
#2nd to show the results
#3rd to calculate and add the new vote to the list
if ($FORM{'show'} eq "vote") { &vote; }
elsif ($FORM{'show'} eq "results") { &results; }
elsif($FORM{'show'} eq "addvote") { &addvote; }
else {
print "Error: Action Undefined!";
}
In the code above, you see that I used an if/else statement that executes
a function (subroutine) based on the value of $FORM{'show'} (which we learned
how to define). Simple enough: we call the appropriately named corresponding
subroutine, and that's what we'll work on next.
Voting
What information do we want to display on the "vote" page? It's best to plan
out an HTML page and transfer the code over to the CGI script subroutine
than to "hard-code" it cold (unless of course, you know your HTML pretty
well).
Let's begin by figuring out what we want on the voting page - the poll question
and topics. You may be familiar with a format like:
What is the most annoying aspect of a website/page?
__ Long Loading Times
__ Error Messages
__ Pop-up Advertisements
__ Too Many Advertisements in General
__ Lack of Content - Under Construction
__ Broken Links
which can appear in a paper or email survey, but with a web page, you can
make it a little more interactive:
Notice that you can only select one choice (inherent property of these radio
buttons). Also check out the source code:
<FORM Action="poll.cgi" Method=post>
<B>What is the most annoying aspect of a
website?</B><BR>
<INPUT type=radio name=choice value=1> Long Loading
Times<BR>
<INPUT type=radio name=choice value=2> Error
Messages<BR>
<INPUT type=radio name=choice value=3> Pop-up
Advertisements<BR>
<INPUT type=radio name=choice value=4> Too Many
Advertisements in General<BR>
<INPUT type=radio name=choice value=5> Lack of Content
- Under Construction<BR>
<INPUT type=radio name=choice value=6> Broken
Links<BR>
<INPUT type=submit value="Vote">
</FORM>
We will use choice numbers to identify which choice is which (easier to match
up than the entire choice phrase - "Error Messages," "Broken Links" etc -
itself). To make it easier to edit, we will want to store the poll's information
in separate files. To make it easiest to work with, we should require 1 file
for the poll topic, another file for the choices and the third(last) for
saving IP addresses (making sure that the voter only votes once).
In our example, we will only be using very basic formatting so as to show
you how the poll script interacts with files and user input.
sub vote {
#We want this subroutine to print out an HTML page with
#the poll question, and all its choices. The poll data is stored
#in 3 different files:
#(1) poll.txt - stores the survey/poll
question
#(2) choices.txt - stores the choices
and current number of votes
#(3) ip.txt - stores the ip addresses
of those who have already voted
#Routine: Get the data from the file, put the data into variables
#In this example, each piece of data is separated by a ( | ) line
#Since there is only one line of text, it should be fairly safe to assume
#that the line can be copied over to a scalar variable ($polldata).
open(POLL, "poll.txt");
$polldata = <POLL>;
close(POLL);
($question,$totalvotes) = split(/\|/,$polldata);
#Each line in the choice file will contain 3 pieces of information:
#1. the choice
#2. the number of the choice (1,2,3, etc for first, second, third etc)
#3. the number of current votes for each choice
open(POLL,"choices.txt");
@polldata = <POLL>;
close(POLL);
print "<form action=\"poll.cgi\" method=post>\n";
#The following will print out each poll choice.
foreach $choice(@polldata) {
chomp($choice);
($pollchoice,$number,$votes) = split(/\|/,$choice);
print "<input type=radio name=\"choice\" value=\"$number\">
$pollchoice<br>\n";
}
print "<input type=submit value=\"Vote\">\n";
print "</form>";
}
Results Display
What should the results page show? The current poll voting statistics.
You don't have to make your voters wait to see the results (that may or may
not be a total turn-off). Every time someone visits
http://www.yoursite.com/poll.cgi?show=results, you simply tell the
script to check up on the current statistics and print those numbers (and
all related information) on a "dynamically generated" page through the CGI
script.
What do you need to do? The general idea is to get the data (as we did in
the vote subroutine) and to print it out with the current number of votes
(the one thing we didn't do in the vote subroutine). A very simple version
might look like:
sub results {
open(POLL, "poll.txt");
$question = <POLL>;
close(POLL);
print "$question<p>\n";
open(POLL,"choices.txt");
@polldata = <POLL>;
close(POLL);
foreach $line(@polldata) {
chomp($line);
($choice, $number, $votes) = split(/\|/, $line);
print "$choice ($votes)<br>\n";
}
}
Click here to see the results:
poll.cgi?show=results
Notice that it simply prints out the poll/survey question, its choices and
the number of votes for each choice. What about those cool bars that you
see at many professional polling sites? Well, now that we've gone through
(and hopefully mastered) the basic output format for the results, we'll be
add this feature in.
What we do know: the number of votes for each choice. This is what we'll
use to convert to bar widths (which depend on the number of votes there are
to make them relative to each other). What we want to do is convert it to
a percentage scale of up to 100.
That would mean finding the total number of votes (first adding all the votes
for each choice together) so that:
| $percentwidth = 100 x |
$votes |
| $totalvotes |
But remembering math, we have to make sure that $totalvotes does NOT equal
zero - which would make the fraction undefined, resulting in an script error.
(Hint: using an if/else loop works)
if ($totalvotes) { $percentwidth = 100 * int($votes/$totalvotes); }
else { $percentwidth = 0; }
Now we have our width percentage (an integer between 0 and 100). You can
adjust that as much as you need; we will be doubling the amount of that width
percentage in our example just so that it will fill up enough of the page:
$width = $percentwidth * 2;
We can now use this with an image to create the bars. Each bar would be an
image with a tag
Image:
This is by far, the less complicated way to create a bar :) For this you
should probably choose an absolute width - something simple like: $width
= $percentwidth * 2, however, you should force this to be an integer value
by casting: $width = int($percentwidth * 2);
Then when outputting (in the foreach loop) we would add an image and specify
the $width to be the image's width:
print "$choice <img src=\"bar.gif\" width=\"$width\"> ($votes votes)";
To make these bars align together, we can print them out in a table; back
to the loop:
print "<table width=80%>\n";
foreach $line(@polldata) {
chomp($line);
($choice, $number, $votes) = split(/\|/, $line);
print
"<tr><td>$choice</td><td><img src=\"bar.gif\"
width=\"$width\"> ($votes votes)</td></tr>";
}
print "</table>\n";
Table:
To do this, you must first specify the total width of the table in the
<TABLE> tag (see the
Tables HTML tutorial
for further information). The table should have at least 2 columns for:
| Poll/Survey Choice |
Vote Percentage Bar |
Out of the 100% of this table with a total width of 250 (just a width I chose
- you can choose any that fits your site best), let's say that the number
of votes for this choice turns out to be 70%. The percentage width would
be 70.
| -----100% = WIDTH of Table----- |
| 70% -add bar color |
N/A |
You would want to make a mini table like the second row of the table
shown above so that we have a nested table for each Poll/Survey choice:
Back to the foreach loop:
print "<table width=80%>\n";
foreach $line(@polldata) {
chomp($line);
($choice, $number, $votes) = split(/\|/, $line);
print
"<tr><td>$choice</td><td> <!-- create mini table
-->";
print "<table width=150 cellspacing=0 cellpadding=0>\n";
print "<tr><td width=\"$width\"><img
src=\"/1x1.gif\"></td></tr></table> <!-- end mini
table -->";
print "<tr></td></tr>";
}
print "</table>\n";
Adding Your Vote
What happens when someone submits their choice to your poll? When he/she
hits the 'Vote' button, the form (whose ACTION attribute is "poll.cgi") calls
the script (poll.cgi) and passes the form's information - the number of the
choice you chose, and a hidden value <INPUT TYPE=hidden NAME=show
VALUE=addvote> - to it. As long as this information is given, you're all
set.
Once the script gets the information, you just have to update one file:
choices.txt, which holds the number
of votes for each choice. Let's give it a run through:
sub addvote {
#An arbitrary checking feature that makes sure that
#a person doesn't spam the poll by voting more than once:
#Open up your IP address log file ip.txt and save that information
#in an array. Then go through each array entry to make sure
#that the current voter's IP address does not appear in this list.
open(IP,"ip.txt");
@ips = <IP>;
close(IP);
foreach $ipaddress(@ips) {
chomp($ipaddress);
if ($ipaddress eq $ENV{'REMOTE_ADDR'}) { print "You have already voted!
Please vote only once.\"; exit; }
}
#If the ip address $ipaddress had matched the IP address
#stored in the environment variable
$ENV{'REMOTE_ADDR'}
#then the program would have exited (stopped execution).
#Otherwise, it will finish the loop and continue with the following:
#Get the original information
open(POLL,"choices.txt");
@polldata = <POLL>;
close(POLL);
#Then rewrite the file with the poll data
#making sure to increment the vote count at the right choice
$totalvotes = 0;
open(POLL,">choices.txt");
foreach $choice(@polldata) {
chomp($choice);
($thechoice, $number,$votes) = split(/\|/, $choice);
if($number eq $FORM{'choice'}) {
$votes++;
}
print POLL "$thechoice|$number|$votes\n";
}
#Once you are done with that, you'll want to show the voter
#the current results - so just run the result subroutine now:
&results;
}
Vote on This Poll
See the Current Results
If you got through this case study fine, you might want to venture off into
some more advanced features. Nevertheless, stay tuned for another installment
when we talk about adding a web-based editing interface, allowing user input
and voters to add their own choices to the poll!
Last Updated August 3, 1999

Perl Case Study - Password CGI