A Walthrough of the Voting Booth's Source Code
    Created: 1332304025615.44
A Walthrough of the Voting Booth's Source Code

About ObjectCloud

ObjectCloud's voting booth system is built on top of an embedded SQlite database.  Each .vote file is a unique SQlite file.  The voting system uses server-side Javascript to prevent a malicious user from "stuffing the box."

Server-Side code: /Classes/vote

/Classes/vote contains all server-side code for a voting booth.  It is applied to all files objects that have a .vote extension.  This section attempts to highlight coding techniques, but doesn't cover the entire source code file.

Setting up the embedded database schema

The first part of the Javascript checks the embedded database's version.  If it hasn't been set, it creates the schema.  Calls to base.PostQuery() must be called as the .vote file's owner, and must be called in an elevated security context.  Once the schema is created, the version is assigned to 0.

elevate(function()
{
   var version = base.GetVersion_Sync({});

   // Constructor
   if (null == version)
      lockMe(function()
      {
         callAsOwner(function()
         {
            // Need to double-check in case someone else built the DB...
            var version = base.GetVersion_Sync({});

            if (null == version)
            {
               base.PostQuery_Sync({query:"create table Question (qId integer not null primary key AUTOINCREMENT, text not null)"});
               base.PostQuery_Sync({query:"create table AuthenticatedVotes (qId not null, voterId not null, timestamp integer not null)"});
               base.PostQuery_Sync({query:"create table AnonymousVotes (qId not null, timestamp integer not null)"});
               base.PostQuery_Sync({query:"create table Options (theOptions not null)"});

               var defaultOptions =
               {
                  canChangeVote: false,
                  canSeeResultsWithoutVoting: false,
                  title: "",
                  question: ""
               };

               base.PostQuery_Sync({query:"insert into Options (theOptions) values (@theOptions)", "@theOptions": JSON.stringify(defaultOptions)});

               base.SetVersion_Sync({version:0});
               version = 0;
            }
         });
      });
});

When constructing the schema, the entire object is locked.  The version is checked inside and outside of the lock in case multiple requests attempt to create the schema.

Getting Poll Results

SQL is used to count the results.  In this case, when getting the results, we use a compound query that has 3 sub-queries:

From the getVotes() function:

elevate(function()
{
resultsFromDB = base.PostQuery_Sync( { query: "select qId from Question group by qId;" + "select distinct qId, count(qId) as numVotes from AuthenticatedVotes group by qId;" + "select distinct qId, count(qId) as numVotes from AnonymousVotes group by qId;" });
});

var resultsToReturn =
{
authenticated: aggregate(resultsFromDB[0], resultsFromDB[1]),
anonymous: aggregate(resultsFromDB[0], resultsFromDB[2])
};

The contents of aggregate are shown for your convenience:

function aggregate(validQIds, results)
{
var toReturn = {};

for (var ctr = 0; ctr < validQIds.length; ctr++)
toReturn[validQIds[ctr].qId] = 0;

for (var ctr = 0; ctr < results.length; ctr++)
if (null != toReturn[results[ctr].qId])
toReturn[results[ctr].qId] = results[ctr].numVotes;

return toReturn;
}

base.PostQuery() always returns a compound array.  For compound queries, the most significant index corresponds with the specific query; for single queries the most significant index is always 0.  The inner array is always an array of objects returned from the query.  When a query returns a scalar, the scalar is used instead of the inner array.

Getting the Current User's Vote

A simpler query is to get the current user's vote:

getVote.webCallable = "GET";
getVote.minimumWebPermission = "Read"; // In case someone's permission changed! What if someone closed voting by revoking Write?
getVote.webReturnConvention = "JavaScriptObject";
function getVote()
{
   var userMetadata = getConnectionMetadata();
var qId = null;

if (userMetadata.name != "anonymous")
elevate(function()
{
var votes = base.PostQuery_Sync( { query: "select qId from AuthenticatedVotes where voterId = @voterId", "@voterId": userMetadata.id })[0];

if (votes.length > 0)
qId = votes[0].qId;
});

return qId;
}

The above query still returns an outer array, thus votes is assigned to the 0th element of base.PostQuery's results.  Another interesting difference is that this query uses symbolic arguments.  SQlite replaces @voterId with the value passed in to the second argument's array.  This technique avoids SQL injection attacks.

Note that the above function sets the webCallable, minimumWebPermission, and webReturnConvention values for the getVote function.  These values are needed in order to expose the getVote function to the web.

Storing Options

The voting booth's options are stored as a serialized JSON object in a table with one row.  This keeps the schema simple, and allows for simple addition of options without needing to update the schema.

setOptions.webCallable = "POST_application_x_www_form_urlencoded";
setOptions.minimumWebPermission = "Administer";
setOptions.parser_canChangeVote = "bool";
setOptions.parser_canSeeResultsWithoutVoting = "bool";
function setOptions(canChangeVote, canSeeResultsWithoutVoting, question, title)
{
var options = getOptions();

if (null != canChangeVote)
options.canChangeVote = canChangeVote;

if (null != canSeeResultsWithoutVoting)
options.canSeeResultsWithoutVoting = canSeeResultsWithoutVoting;

if (null != question)
options.question = question;

if (null != title)
options.title = title;

elevate(function()
{
base.PostQuery_Sync( { query: "update Options set theOptions=@theOptions", "@theOptions": JSON.stringify(options) });
});
}

function getOptions()
{
var options;
elevate(function()
{
callAsOwner(function()
{
options = base.PostQuery_Sync( { query: "select theOptions from Options" })[0][0];
options = eval('(' + options + ')');
});
});

return options;
}

Because PostQuery always returns a compound array, the single row from the Options table is element [0][0].  theOptions, a serialized JSON string, is eval()ed to parse.  Because the string comes from trusted sources, we can use eval() instead of JSON.parse().

The Template .vote File, /Templates/Voting Booth Template.vote

The template .vote file, /Templates/Voting Booth Template.vote is copied whenever a new voting booth is created.  The file was created with the following steps:

  1. In /Templates, a new file was created of type "Database."  The name was "Voting Booth Template.vote"
  2. The new file was viewed to stimulate creating its schema

The following line was added to /Shell/Navigation/Directory.json so that users can create a new Voting Booth:

{"Template":"/Templates/Voting Booth Template.vote","Extension":".vote","Display":"Voting Booth (Experimental)"}

Viewing a .vote file with SQL

It's possible to use Whisquil, a simple SQL shell, to view the contents of a .vote file.  To do so, use the following URL syntax:

/Shell/Editors/Whisquil.wchtml?FileName=[Path to .vote file]

Narl, the .vote Editor

Narl, (pronounced "nar-eye",) allows someone to set the valid responses and options for a voting booth.  Narl is stored at /Shell/Editors/Narl.wchtml

Narl loads the Javascript object to access the server-side code with the following <? Scripts... tag:

<? Scripts(/API/Prototype.js, <? $_GET["FileName"] ?>?Method=GetJSW&assignToVariable=VotingBooth, /API/nicEdit.js) ?>

The options and questions are pre-loaded using WebComponents:

var pollInfo = <? WebComponent($_GET["FileName"] . "?Method=getQuestionsAndOptions") ?>;
var options = pollInfo.options;
var questions = pollInfo.questions;

Options are written back to the server using the VotingBooth object that wraps all AJAX.  (Note:  Prototype.js gives the $("...") syntax)

function updateOptions()
{
$("OptionsEditor").disable();

var question = $("question").innerHTML;

VotingBooth.setOptions(
{ canChangeVote: $("canChangeVote").checked, canSeeResultsWithoutVoting: $("canSeeResultsWithoutVoting").checked, question: question, title: $("q_title").value, }, function() { $("OptionsEditor").enable(); options.question = question; }); }

A question is added and updated using the VotingBooth object as well:

function addQuestion()
{
var newQuestion = $("AddQuestionInput").value;
$("AddQuestionInput").value = "";

VotingBooth.addQuestion(
newQuestion,
{},
displayQuestion);
}

function updateQuestion(qId)
{
var text = $("q" + qId).value;
VotingBooth.updateQuestion(
qId,
text,
{},
function(question)
{
$("q" + qId).value = question.text;
});
}

Goreal, the voting booth Program

Goreal, (pronounced "Gore-al,") allows someone to vote.  Goreal is stored at /Shell/Viewers/Goreal.wchtml

Goreal loads the Javascript wrapper, questions, and options just like Narl.

Goreal loads the currently-submitted vote with a WebComponent.  If the user hasn't voted, it's set to null.

var vote = <? WebComponent($_GET["FileName"] . "?Method=getVote") ?>;

The vote is submitted through AJAX:

function submitVote()
{
var voteToSubmit = null;

questions.each(function(question)
{
if ($("q" + question.qId).checked)
voteToSubmit = question.qId;
});

if (null != voteToSubmit)
VotingBooth.vote(
voteToSubmit,
{},
function()
{
window.location.reload(true);
});
else
alert("Please select a question");
}

Results are also loaded through AJAX.  In the event that the query takes awhile to run, the user will still see a page:

if (options.canSeeResultsWithoutVoting || null != vote)
VotingBooth.getVotes(
{},
function(votes)
{
questions.each(function(question)
{
var qId = question.qId;
var qSpan = $("s" + qId);

qSpan.innerHTML = ': <span style="font-size: 1.5em">' + (votes.authenticated[qId] + votes.anonymous[qId]) + '</span><span style="font-size: 0.75em"> (' +
votes.authenticated[qId] + ' authenticated, ' + votes.anonymous[qId] + ' anonymous)</span>';
});
});

About ObjectCloud