Poll Module Design
by
Mark Dalrymple
I. Essentials
II. Introduction
People like to offer their opinions, and one of the easiest ways to do
that is to vote in an opinion poll. The Poll module lets you construct
polls that are active for a range of dates, specify the choices the
users can make, whether to require the voter to be a registered user,
and display the current results. Since site designers like to change
things, all of the user-accessible portions of the polls use the ACS
style mechanism along with an API for controlling the display.
Polls are for multi-item, single-choice, single-vote opinion polls.
They're not designed to do anything else e.g. M items to choose from
and the user can choose N of them.
Site Publishers have a set of tools at their disposal for creating
and modifying the polls, as well as a set of anti-abuse tools for
removing votes made by ballot-box stuffers.
III. Historical Considerations
The Poll Module was originally a client-driven project which was
integrated into ACS. The flexibility of display as well as the anti
abuse tools were driven by client needs.
VI. Data Model Discussion
Three tables are used. One to hold the polls, one to hold the
choices the user can make for each pool, and the actual set of
votes users have made.
create table polls (
poll_id integer not null primary key,
name varchar(100) not null,
description varchar(4000),
start_date date,
end_date date,
require_registration_p char(1) default 'f' check (require_registration_p in ('t','f'))
);
Any number of polls can be active at a time. Whether a poll is active
depends on the start_date and end_date. To disable a poll, use the
admin pages to set the end_date into the past or the start_date into
the future. Name is a short name used to label pages and links, while
description is a longer description that is displayed with the votes
and results.
require_registration_p indicates whether a user has to be a registered
user (have an entry in the users table) before they can vote. Making
registration required implies that the user can only vote once for
each poll.
If registration is not required, we can't distinguish one vote
from another, so users can vote any number of times for each poll.
Why not use the IP address to control whether someone can vote more
than once, even if they're not a registered user? Unfortunately
there's not a 1-to-1 mapping of IP addresses to people. IP
masquerading hides many people behind one IP address (meaning we would
prevent legal votes), and users behind massive proxy systems (such as
AOL users) can get a random IP address on each request, (meaning
obnoxious folks can vote multiple times).
create table poll_choices (
choice_id integer not null primary key,
poll_id references polls not null,
label varchar(500) not null,
sort_order integer
);
This holds the choices that users can vote on. The sort_order column
controls the order of presentation for the choices associated with a
given poll.
create table poll_user_choices (
poll_id references polls not null,
choice_id references poll_choices not null,
-- user_id can be NULL if we're not requiring registration
user_id references users,
ip_address varchar(50) not null,
choice_date date not null
);
Each user vote is recorded here. If there is no user ID (meaning that
the voter is not a registered user), the user_id in this table is
NULL. Even though we don't use the IP address to control user
voting, we keep it around in case some obnoxious person stuffs
the ballot box. We can go into SQL*Plus and undo the damage, or
use some of the admin pages to clean up after the evildoer.
There are also sequences for poll identifiers and poll choice identifiers.
There are three indices used to speed up queries:
create index poll_choices_index on poll_choices(poll_id, choice_id);
create index poll_user_choice_index on poll_user_choices(poll_id);
create index poll_user_choices_choice_index on poll_user_choices(choice_id);
VII. Legal Transactions
The Poll tables have referential integrity (foreign key constraints),
thereby preventing situations where a poll item doesn't have an owning
poll or a vote doesn't have a corresponding poll or item.
Administrative Transactions
The administrative pages have complete control over the data in the
database.
Creating a new poll:
insert into polls
(poll_id, name, description,
start_date, end_date, require_registration_p)
values
(...)
Editing an existing poll:
update polls set
name = :name,
description = :description,
start_date = :start_date,
end_date = :end_date,
require_registration_p = :require_registration_p
where
poll_id = :poll_id
Deleting a poll:
delete from poll_user_choices where poll_id = :poll_id
delete from poll_choices where poll_id = :poll_id
delete from polls where poll_id = :poll_id
Creating a new poll choice:
insert into poll_choices
(choice_id, poll_id, label, sort_order)
values
(...)
Changing poll item sort order:
foreach i [array names choice_order] {
...
update poll_choices
set sort_order = :choice_order
where choice_id = :i
}
Deleting a poll choice:
delete from poll_choices
where choice_id = :choice_id
Deleting anonymous duplicate votes (based on IP address):
delete from poll_user_choices
where poll_id = :poll_id
and user_id is null
and (choice_id, ip_address) in
(select choice_id, ip_address
from poll_user_choices
where user_id is null
group by choice_id, ip_address
having count(*) >= :deletion_threshold)
End User Transaction
Recording a Vote:
set insert_sql "insert into poll_user_choices
(poll_id, choice_id, user_id, choice_date, ip_address)
values
(...)
VIII. Templates and Customization
The templates for the user-accessible pages are stored in
/template/poll. The templates are named "template-name.plain.adp" and
"template-name.fancy.adp". The ACS style mechanisms choose which
template to use.
There are two categories of templates in use: those that display the polls,
the choices, and the results; and those that say "you did something wrong".
- index: what is used for the top-level page in /poll.
- one-poll: display one poll for voting.
- poll-results: show the current results
- vote: thanks for voting
- already-voted: the user has already voted in a "registration required" poll
- novote: The user didn't chose a radio button on the one-poll page
- dberror: something bad happened in storing the vote in the database.
All templates are passed the page_title, header_image, and context_bar variables.
These templates get extra variables:
- index: polls (which should be passed to [poll_front_page])
- one-poll: poll_name, poll_description, choices (which should be passed to
[poll_display]), form_html (which should be output after the <form>)
- poll-results: poll_name, poll_description, values (which should be passed to [poll_results]), poll_id (which should be used in links), total_count (the total number of votes)
- vote: poll_id
- already-voted: poll_name, poll_description, poll_id
- novote: poll_id
- dberror: errmsg (the error string from the database)
Customization Functions
To fully customize Poll template, designers need to
know how to invoke functions as well as how to put variables into
<%= $variable >
units. There are three
functions provided that take the database-driven output (e.g. the currently
active polls) and some customization information and then return a blob of
html for inclusion. Designers would do something like
<%= [poll_display -item_start "<tr><td>" -item_end "</tr>" -style_start "<font color=white><i>" -style_end "</i></font>" $choices] %>
So why not use ad_register_styletag
and include tweakable parameters
in the tagset? ADPs have a severe limitation in that html <tags> embedded
in the styletag cause ADPs to prematurely end the parsing of the tagset. That is, this:
<my-tag foo="<b>" bar="<li>" baz="<font face=happy>"></my-tag>
has a tagset that consists of "foo=<b"
, and a content-string of
everything else.
To allow customization of each line of database-driven output, say whether to
arrange the available choices in an <ul>
or in
a table, not being able to include arbitrary html is major loss.
Instead, three functions are provided. Each takes optional parameterized
arguments and a required blob of data which is passed to the template ADP
by the Tcl pages in /poll.
poll_front_page ?optional arguments? polls
Use this function to control the display on the "index" page (the top-level page
you get when going to /poll). If invoked without optional arguments,
the polls are arranged in an unordered list. The "polls" variable is provided to
the index template.
This code:
<ul>
</ul>
results in this:
What Would Rolf Do?
What's Your Favorite Color? (registration required)
While this:
<table border=1 bgcolor=silver>
<%= [poll_front_page -item_start "<tr><td>" -style_start "<font color=green>" -style_end "</font>" -require_registration_start "<td>" -require_registration_text "<font color=green>Registration Mandatory!!!</font>" $polls] %>
</table>
results in this:
The arguments:
- -item_start: text to be emitted before each poll name. Usual uses are <li>
or <tr><td> Defaults to <li>
- -item_end: text to be emitted after each poll name.
- -style_start: text to be emitted immediately before the poll name.
Here's where you would put <font> directives or other text formatting commands
- -style_end: text to be emitted immediately after the poll name. You'd
put </font> tags and the like here.
- -require_registration_text: what to display if the poll requires registration.
Defaults to "There are no currently active polls"
- -require_registration_start: text to be emitted immediately before the
require_registration_text. You can put text formatting and/or html
structural tags (like making a new table row or column before the
require_registration_text)
- -require_registration_end: text to be emitted immediately after the
require_registration_text.
poll_display ?optional arguments? choices
Use this function to control the display of an individual poll. If
invoked without optional arguments, the poll choices are arranged in an
unordered list. The "choices" variable is provided to the one-poll template.
This code:
<ul>
</ul>
results in this:
While this:
<table border=3 text=white bgcolor=black>
<%= [poll_display -item_start "<tr><td>" -item_end "</tr>" -style_start "<font color=white><i>" -style_end "</i></font>" $choices] %>
</table>
results in this:
The arguments:
- -item_start: text to be emitted before each choice name. Usual uses are <li>
or <tr><td> Defaults to <li>
- -item_end: text to be emitted after each choice name.
- -style_start: text to be emitted immediately before the choice name.
Here you would put <font> directives or other text formatting commands
- -style_end: text to be emitted immediately after the choice name. You'd
put </font> tags and the like here.
- -no_choices: text to be emitted if there are no choices in the poll. This
is really an administration/configuration problem. Defaults to "No Choices Specified"
poll_results ?optional arguments? results
Use this function to control the display on the "results" page. This function
is a wrapper around gr_sideways_bar_chart which simplifies the API.
This code
results in this:
|
|
|
Eat Cheese | | 40
|
Go Skateboarding | | 26.7
|
Wait for Cable Modem installer | | 33.3
|
While this:
<table bgcolor=pink border=3>
<tr>
<td width=300>
<%= [poll_results -bar_color purple -display_values_p "f" -display_scale_p "f" -bar_height 30 $values] %>
</table>
results in this:
| | | Eat Cheese | |
| Go Skateboarding | |
| Wait for Cable Modem installer | |
|
|
The arguments:
- -bar_color: what color to display the bar. Can be blue, dark-green, purple, red, black, orange, or medium-blue. Defaults to blue.
- -display_values_p: a "t" or "f" value. Should the percentages of the vote
be displayed. Defaults to "t"
- -display_scale_p: a "t" or "f" value. Should the "0 to 100" scale be
displayed at the top of the results. Defaults to "t"
- -bar_height: how tall to make the bars. Defaults to 15.
Note that some specific customizations aren't possible now, such as
putting the total number of votes after each bar on the chart, and
using multiple pictures for each bar (like what the Slashdot poll does) due to
limitations in the API of gr_sideways_bar_chart.
IX. Configuration/Parameters
There are no additional configuration parameters
X. Acceptance Tests
Probably should just include the existing acceptance for the Poll
Module. I don't know where that's kept these days
XI. Authors
The initial creation of the Poll module was by Ben Adida. Mark Dalrymple
extended Ben's work and added the templating facilities. Philip Greenspun
added the voting abuse clean-up features.
markd@arsdigita.com.com