AJAX Suggestion Voter

This Page has been Viewed: 546 Times
Login to Vote

AjaxVoter

 

Introduction

This template is a simple AJAX enabled Up/Down or Positive/Negative Voter.  You are able to place many of these on one page to simulate the FeVote functionality.  Since this uses page properties to save the voting results its not recommended for any use if security or 100% integrity maybe a concern.  In order to vote the voter must have write capabilites for the page the results are being stored on.  As written this voter can only be used by users that are logged in, no anon voting is allowed.  It should also be noted that if a user votes twice their vote does not get counted twice instead their vote is changed.  Please direct any questions / comments to this forum thread.

How the Score is calculated

There are 2 methods of scoring "sum" and "percent" the default method is "sum".  With the "sum" scoring method each positive vote is given a value of 1 and each negative vote is given a value of -1.  The Score is calculated by the sum of all the positive and negative votes.  For Example:  Billy votes Positive or ThumbsUp, his vote will be given a value of 1.  Jane votes Negative or ThumbsDown, her vote will be given a value of -1.  The score for Billy and Jane's votes will be (1 + -1 = 0), if John comes along and votes ThumbsUp then the score would be +1 (1 + -1 + 1 = 1).  The "percent" scoring method simply displays the percentage of ThumbsUp votes.

History

Version Date Author Description
1.00 12/22/2009  rberinger/NeilW Initial Release 
1.01 1/11/2010 NeilW CSS fixed for IE7

Requirements

  • This template requires MindTouch version: 9.02 Lyons or Greater

How do I install it?

These instructions will help you install the template 'AjaxVoter' on your wiki quickly and correctly.

Perform the following steps:

  1. Copy the template source code:
    1. If the code in the "HTML Source" area below is not already visible and selected, click the "View" button to display and select it.
    2. Copy it to your clipboard (CTRL-C).
  2. Create a new template on your wiki:
    1. Select the Tools->Templates menu item on your wiki
    2. Click "New Page" button.  This will open the editor on a new page titled (for the moment) "Template:Page Title".
  3. Paste in the source code:
    1. In the editor, click the "Source" button to view the HTML source of the page.
    2. Select all text (CTRL-A)
    3. Paste in the template source code from the clipboard (CTRL-V).
  4. Click "Save" button.

HTML Source for Template:AjaxVoter

Additional installation instructions

  • Right Click and Download the 2 image files below and copy to a page in your own MindTouch Installation.
  • Find the following lines in the template you copied above and change the image locations to point to the images you uploaded on your MindTouch Installation.
    • <a href="#" id=('up_val_' ..@id)><img src="http://developer.mindtouch.com/@api/deki/files/5004/=Thumbs_upSQ.png" style="height:24px;width:24px;" /></a>       
      <a href="#" id=('down_val_' ..@id)><img src="http://developer.mindtouch.com/@api/deki/files/5005/=Thumbs_downSQ.png" style="height:24px; width:24px;" /></a>

How do I use it?

There are 2 properties created for each AjaxVoter.  One that is controlled by the "name" argument and another that is the voterid argument preceded with "Totals@".  i.e. If you created a voter with the name of TestVoter and a voterid of "TestVoter_id" there will be 2 properties created, "TestVoter" and "Totals@TestVoter_id".  The Totals@voterid property is a map that has 3 values {negative, positive, score}.  The Totals@ property allows you to quickly gather score totals without iterating the voter property list of maps to get the totals.  This would possibly be useful for vote summary pages or reports.   The unique voterID allows you to reset the voter by changing its name but keeping the Totals@ property the same so you do not have to change any summary pages gathering those results.

To help clarify the intended use of the Totals@ property I will try to layout a scenerio using a documentation page as an example:

As you can see I have a page rating voter at the top right of this documentation page.  Over time the score will change when people vote this template up or down.  If I notice a negative or low score that should indicate some much needed changes are required.  Once I make those changes I may want to "reset" the counter.  All I need to do is change the name parameter and the score will be reset.  However if I was to view a previous version of this page I will be able to see the score prior to the name change.  At the same time the voterid stays the same and the Totals@ property will get reset along with everything else but any slave functions (such as summary pages or templates) do not need to change because I only changed the name of the voter and not the voterid.  I know I'm almost confused myself but sit and picture it in your mind and all should become clear.

Arguments

Name
Type
Default
Description
 name  str? voter  The Property name to save the Voter results under.  This argument is only REQUIRED if more than one voter is present on a page or if the vote result properties are stored on another page that is also housing another voter.
 pageid  num? page.id   The Page ID of the page to store the results of the voting 
 voterid  str?  n/a REQUIRED: This argument is a required argument.  It should be unique between all the voters on a single property destination page.  This allows for the totals@ property to stay with a particular voter even if the voter name changes.  Useful if you want to reset the voter scores by changing its name but keeping versioning intact while at the same time not requiring the change of any summary reports that rely on the Totals@ property for its results. 

 

Examples

{{ AjaxVoter{ name:'css_voter', voterid:'cssvoter_id', scoring:'sum' } }} This example uses the sum scoring method, which simply sums the positive (value = 1) and Negative (Value = -1) votes to provide a numeric score.  Hovering the mouse over the score will provide the actual percentage of Positive Vs. Negative votes.
{{ AjaxVoter{ name:'example_voter', voterid:'examplevoter_id', scoring:'percent'} }} This example uses the "Percentage" scoring method.  The score range will be from 0% - 100% according to the number of positive votes.
Login to Vote
This example uses the sum scoring method, which simply sums the positive (value = 1) and Negative (Value = -1) votes to provide a numeric score.  Hovering the mouse over the score will provide the actual percentage of Positive Vs. Negative votes.
Login to Vote
This example uses the "Percentage" scoring method.  The score range will be from 0% - 100% according to the number of positive votes.

Reference(s)

  Please direct any questions / comments to this forum thread..

Credits/Special Thanks

Thanks to NeilW for his aesthetics insights and his work for the percentage scoring while developing this template.

Template/Extension Syntax Highlighted View

DekiScript Source for Template: AjaxVoter

//  ***** THIS TEMPLATE IS CURRENTLY IN DEVELOPMENT DO NOT USE *****

<input type="hidden" id=('TheProperty_' ..@id) value=($0 ?? $name ?? 'voter') />
<input type="hidden" id=('pageApi_' ..@id) value=($1 ?? $pageid ?? wiki.getpage(page.id).api) />
<input type="hidden" id=('CurUserID_' ..@id) value=(user.id) />
<input type="hidden" id=('UniqueId_' ..@id) value=($2 ?? $voterid ?? '') />

var scoretype = ($3 ?? $scoring ?? 'sum');
if(scoretype != 'sum' && scoretype != 'percent') {let scoretype = 'sum'}
<input type="hidden" id=('Scoring_' ..@id) value=(scoretype) />

if(($3 ?? $voterid ?? '') == '') {
    <p>    
        'Template:AjaxVoter (ERROR):  The $voterid Parameter MUST have a value and should be unique amongst all the AjaxVoters on this page.';
    </p>
} else {

<table>
<tr>
<td style="padding:0px; border:1px solid black; min-width:75px; width:75px">
<div id=('voter-container_' ..@id) style="padding-bottom:5px">
    <div id=('vote-results-container_' ..@id)>
        <div id=('vote-results-percent_' ..@id) style="font-size:.8em;font-weight:bold;margin:auto;text-align:center"/>
        <div id=('vote-results_' ..@id) style="font-size:1.5em;font-weight:bold;margin:auto;text-align:center"/>
        <div id=('vote-loading_' ..@id) style="margin:auto;width:16px;height:16px;background-repeat: no-repeat; background-image:url('/skins/common/icons/anim-wait-circle.gif');" />
    </div>
    <div id=('results-footer_' ..@id) style="font-weight:bold;font-size:.8em;text-align:center;"></div>
    <div id=('icon-wrap_' ..@id) style="text-align:center;">
        if(!user.anonymous) {
            <a href="#" id=('down_val_' ..@id)><img src="http://developer.mindtouch.com/@api/deki/files/5005/=Thumbs_downSQ.png" style="height:24px;width:24px;" /></a>
            <a href="#" id=('up_val_' ..@id)><img src="http://developer.mindtouch.com/@api/deki/files/5004/=Thumbs_upSQ.png" style="height:24px;width:24px;" /></a>        
        }
        else 'Login to Vote';
    </div>
</div>
</td>
</tr>
</table>
}
<html><head>

//    <link rel="stylesheet" type="text/css" href="" />
//    <script type="text/javascript" src="" />

    <script type="text/javascript">"

        $(document).ready(function() {
var scoring = $('#Scoring_"..@id.."').val();
if (scoring == 'sum') $('#vote-results-container_"..@id.."').hover(ShowPercent"..@id..", ShowScore"..@id..");
$('#vote-results-percent_"..@id.."').hide();

$('#up_val_"..@id.."').click(function() {
    var thePageApi = $('#pageApi_"..@id.."').val();
    var thePropertyName = $('#TheProperty_"..@id.."').val();

    ReadProperty( thePageApi, thePropertyName, IncreaseVote_"..@id..");
    return false;
});
$('#down_val_"..@id.."').click(function() {
    var thePageApi = $('#pageApi_"..@id.."').val();
    var thePropertyName = $('#TheProperty_"..@id.."').val();

    ReadProperty( thePageApi, thePropertyName, DecreaseVote_"..@id..");
    return false;
});

GetVoteCount_"..@id.."();

        });

function ShowPercent"..@id.."() {
    $('#vote-results_"..@id.."').hide();
    $('#vote-results-percent_"..@id.."').show();
}
function ShowScore"..@id.."() {
    $('#vote-results-percent_"..@id.."').hide();
    $('#vote-results_"..@id.."').show();
}

var SaveTotals_"..@id.." = function(curVal, status) {

    var voteCounts = '';
    var totalsPos = 0;
    var totalsNeg = 0;
    var thePageApi = $('#pageApi_"..@id.."').val();
    var thePropertyName = 'Totals@' + $('#UniqueId_"..@id.."').val();

    if(typeOf(curVal) == 'array') {
        for(var i = 0; i < curVal.length; i++) {
            if(curVal[i].vote == 1) {
                totalsPos = totalsPos + 1;
            } else {
                totalsNeg = totalsNeg + -1;
            }
        }
        voteCounts = {positive: totalsPos, negative: Math.abs(totalsNeg), score: totalsPos + totalsNeg};
    } else {
        voteCounts = {positive: 0, negative: 0, score: 0};
    }    

    AddUpdateProperty(thePageApi, thePropertyName, voteCounts, '', '');
}


var IncreaseVote_"..@id.." = function(curVal, status) {
    $('#icon-wrap_"..@id.."').hide();
    $('#vote-loading_"..@id.."').show();
    $('#vote-results_"..@id.."').hide();

    var voteCounts = new Array;
    var newUser = true;
    var thePageApi = $('#pageApi_"..@id.."').val();
    var thePropertyName = $('#TheProperty_"..@id.."').val();
    var CurUserID = $('#CurUserID_"..@id.."').val();

    if(typeOf(curVal) == 'array') {
        for(var i = 0; i < curVal.length; i++) {
            if(curVal[i].userid == CurUserID) {
                curVal[i].vote = 1;
                newUser = false;
                break;
            }
        }
    } else {
        curVal = new Array;
    }

    if(newUser == true) {
        curVal[curVal.length] = {userid: CurUserID, vote: 1};
    }

    AddUpdateProperty(thePageApi, thePropertyName, curVal, GetVoteCount_"..@id..", SaveTotals_"..@id..");
}

var DecreaseVote_"..@id.." = function(curVal, status) {
    $('#icon-wrap_"..@id.."').hide();
    $('#vote-loading_"..@id.."').show();
    $('#vote-results_"..@id.."').hide();

    var voteCounts = new Array;
    var newUser = true;
    var thePageApi = $('#pageApi_"..@id.."').val();
    var thePropertyName = $('#TheProperty_"..@id.."').val();
    var CurUserID = $('#CurUserID_"..@id.."').val();

    if(typeOf(curVal) == 'array') {
        for(var i = 0; i < curVal.length; i++) {
            if(curVal[i].userid == CurUserID) {
                curVal[i].vote = -1;
                newUser = false;
                break;
            }
        }
    } else {
        curVal = new Array;
    }

    if(newUser == true) {
        curVal[curVal.length] = {userid: CurUserID, vote: -1};
    }

    AddUpdateProperty(thePageApi, thePropertyName, curVal, GetVoteCount_"..@id..", SaveTotals_"..@id..");
}

var GetVoteCount_"..@id.." = function(parm, status) {
    $('#vote-loading_"..@id.."').show();
    $('#vote-results_"..@id.."').hide();

    var thePageApi = $('#pageApi_"..@id.."').val();
    var thePropertyName = $('#TheProperty_"..@id.."').val();
    var CurUserID = $('#CurUserID_"..@id.."').val();

    ReadProperty( thePageApi, thePropertyName, ShowVoteCount_"..@id..");
}

var ShowVoteCount_"..@id.." = function(curVal, status) {
    $('#icon-wrap_"..@id.."').show();
    $('#vote-results_"..@id.."').show();
    $('#vote-loading_"..@id.."').hide();

// calculate vote totals
    var totalsPos = 0;
    var runTotal = 0;
    var totalVotes = 0;
    var bgcolor;
    var green = new Array(80,225,80);
    var red = new Array(245,110,100);
    var white = new Array(255,255,255);
    var scoring = $('#Scoring_"..@id.."').val();
    if(typeOf(curVal) == 'array') {
        for(var i = 0; i < curVal.length; i++) {
            runTotal = runTotal + curVal[i].vote
            if(curVal[i].vote > 0) totalsPos ++;
        }
        totalVotes = curVal.length;
    }
// calculate and output final score and color
    if (scoring == 'sum') {
        if (runTotal == 0)
            bgcolor = 'rgb(' + white.join(',') + ')';
        else if (runTotal > 0) {
            runTotal = '+' + runTotal;
            bgcolor = 'rgb(' + green.join(',') + ')';
        }
        else if (runTotal < 0)
            bgcolor = 'rgb(' + red.join(',') + ')';
        // update percentage overlay
        var percentPos = Math.round((totalsPos / Math.max(totalVotes,1)) * 100);
        $('#vote-results-percent_"..@id.."').html('Pos: ' + percentPos + '%<br/>Neg: ' + (100-percentPos) + '%').css('background-color', bgcolor);
    }
    else {
        var outColor = new Array(192,192,192);

        if (totalVotes == 0)
            runTotal = 'NA';
        else {
            var degree = 2*totalsPos/totalVotes - 1;
            var i;
            for (i = 0; i < 3; i++)
                outColor[i] = Math.round(white[i] + degree*
                    (degree > 0 ? green[i] - white[i] : white[i] - red[i]));
            if (scoring == 'percent') runTotal = Math.round(100*totalsPos/totalVotes) + '%';
            else                      runTotal = (degree > 0 ? '+' : '') + Math.round(100*degree);
        }

        bgcolor = 'rgb(' + outColor.join(',') + ')';
    }
    $('#vote-results_"..@id.."').html(runTotal).css('background-color', bgcolor);
    $('#results-footer_"..@id.."').html(totalVotes + ' votes');
}

function typeOf(obj) {
  if ( typeof(obj) == 'object' ) {
    if (obj.length) {
      return 'array';
    } else {
      return 'object';
    }
  } else {
      return typeof(obj);
  }
}

// This function is simply used to aid in debugging.
var AlertMe = function(text, status) {

    alert(status + ':' + text);

}

function AddUpdateProperty(pageApi, propName, propValue, callback, totalsCallBack) {

    // property name to update
    var propertyname = 'urn:custom.mindtouch.com#' + propName;
    propValueJson = YAHOO.lang.JSON.stringify(propValue);

    // send AJAX request to find the property
    $.ajax({
    
        // set the uri for the properties API of the current page
        // add parameter to only list properties that match the requested name
        // add parameters to convert response to JSON and
        url: pageApi + '/properties?dream.out.format=json&names=' + Deki.url.encode(propertyname), 
        
        // set the request HTTP verb
        type: 'GET',
        cache: false,
        
        // check outcome of the request
        complete: function(xhr) {
        
            // check response status code
            if(xhr.status == 200) {
            
                // evaluate respone JSON data
                var data = eval('(' + xhr.responseText + ')');

                // read property href and ETag
                var href = data.property && data.property.contents['@href'];
                var etag = data.property && data.property['@etag'];
                
                // check we the value was found
                if(href && etag) {

                    // send AJAX request to update the property
                    $.ajax({ 
        
                        // set the request HTTP verb
                        url: href + '?dream.in.verb=PUT',
                        type: 'POST', 
        
                        // set the value of the updated property        
                        data: propValueJson, 
                        contentType: 'text/plain',
                        processData: false,

                        // add the 'ETag' header which checks if the property was modified since we read it
                        beforeSend: function(xhr) {

                            // NOTE (see Update 1 below): remove content-encoding added by Apache mod_deflate
                            etag = etag.replace('-gzip', '').replace('-bzip2', '').replace('-zip', '');

                            xhr.setRequestHeader('ETag', etag); 
                            return true; 
                        },
        
                        // check response status code
                        complete: function(xhr) {

                            // check the response status code
                            if(xhr.status == 200) {
                                // alert('Property was successfully updated.');
                                if(typeof callback == 'function') callback(true, xhr.status); else return true;
                                if(typeof totalsCallBack == 'function') totalsCallBack(propValue, xhr.status); else return false;
                            } else {
                                // alert('Unable to update the property. It may no longer exist (try recreating it), it may have been changed, or you don\\'t have permission to update it.');
                                if(typeof callback == 'function') callback(false, xhr.status); else return false;
                            }
                        }
                    });
                } else {

                    // CREATE PAGE PROPERTY
                    // send AJAX request using jQuery 
                    $.ajax({
                    
                        // set the uri for the properties API of the current page
                        url: pageApi + '/properties',
                        
                        // set the request HTTP verb
                        type: 'POST', 
                        
                        // set the value of the new property
                        data: propValueJson, 
                        contentType: 'text/plain',
                        processData: false,
                        
                        // add the 'Slug' header which sets the property name
                        beforeSend: function(xhr) { 
                            xhr.setRequestHeader('Slug', 'urn:custom.mindtouch.com#' + propName); 
                            return true; 
                        },
                        
                        // check outcome of the request
                        complete: function(xhr) {
                
                            // check the response status code
                            if(xhr.status == 200) {
                                // alert('Property was successfully created.');
                                if(typeof callback == 'function') callback(true, xhr.status); else return true;
                                if(typeof totalsCallBack == 'function') totalsCallBack(propValue, xhr.status); else return false;
                            } else {
                                // alert('Unable to create the property. It may already exist (try deleting it) or you don\\'t have permission to create it.');
                                if(typeof callback == 'function') callback(true, xhr.status); else return true;
                            }
                        }
                    });  // End Ajax (Create Property)

                }
            } else {
                // alert('Unable to query the property. You don\\'t have permission to read it.');
                if(typeof callback == 'function') callback(false, xhr.status); else return false;
            }
        }
    });  // End AJAX (Update Property)

}  // End Function AddUpdateProperty


function DeleteProperty(pageApi, propName, callback) {

    // property name to update
    var propertyname = 'urn:custom.mindtouch.com#' + propName;

    // send AJAX request to find the property
    $.ajax({
    
        // set the uri for the properties API of the current page
        // add parameter to only list properties that match the requested name
        // add parameters to convert response to JSON and
        url: pageApi + '/properties?dream.out.format=json&names=' + Deki.url.encode(propertyname), 
        
        // set the request HTTP verb
        type: 'GET',
        cache: false,
        
        // check outcome of the request
        complete: function(xhr) {
        
            // check response status code
            if(xhr.status == 200) {
            
                // evaluate respone JSON data
                var data = eval('(' + xhr.responseText + ')');

                // read property href
                var href = data.property && data.property.contents['@href'];
                
                // check we the value was found
                if(href) {

                    // send AJAX request to delete the property
                    $.ajax({ 
        
                        // set the request HTTP verb
                        url: href + '?dream.in.verb=DELETE',
                        type: 'POST', 

                        // check response status code
                        complete: function(xhr) {

                            // check the response status code
                            if(xhr.status == 200) {
                                // alert('Property was successfully deleted.');
                                if(typeof callback == 'function') callback(true, xhr.status); else return true;
                            } else {
                                // Who cares we were going to trash it anyhow
                                // alert('Unable to delete the property. It may not exist (try creating it) or you don\\'t have permission to delete it.');
                                if(typeof callback == 'function') callback(true, xhr.status); else return true;
                            }
                        }
                    });
                } else {
                    // Who cares we were going to trash it anyhow
                    // alert('Unable to find the property. It may not exist (try creating it).');
                    if(typeof callback=='function') callback(true, xhr.status); else return true;
                }
            } else {
                alert('Unable to query the property. You don\\'t have permission to read it.');
                if(typeof callback=='function') callback(false, xhr.status); else return false;
            }
        }
    });

} // End Function DeleteProperty


function ReadProperty(pageApi, propName, callback) {

    // property name to read
    var propertyname = 'urn:custom.mindtouch.com#' + propName;

    // send AJAX request using jQuery
    $.ajax({
    
        // set the uri for the properties API of the current page
        // add parameter to only list properties that match the requested name
        // add parameters to convert response to JSON and
        url: pageApi + '/properties?dream.out.format=json&names=' + Deki.url.encode(propertyname), 
        
        // set the request HTTP verb
        type: 'GET',
        cache: false,
        async: false,
        
        // check outcome of the request
        complete: function(xhr) {
        
            // check response status code
            if(xhr.status == 200) {
            
                // evaluate response JSON data
                var data = eval('(' + xhr.responseText + ')');

                // read property value
                var value = data.property && data.property.contents['#text'];
                
                // check we the value was found
                if(value) {

                    value = YAHOO.lang.JSON.parse(value);
                     // alert('Property was successfully read.\\nIts value is \\'' + value + '\\'');
                    if(typeof callback == 'function') callback(value, xhr.status); else return value;
                } else {
                    // alert('Unable to find the property. It may not exist (try creating it).');
                    if(typeof callback == 'function') callback(value, xhr.status); else return null;
                }
            } else {
                // alert('Unable to query the property. You don\\'t have permission to read it.');
                if(typeof callback == 'function') callback(value, xhr.status); else return null;
            }
        }
    });

}
        
    "</script>

    <style type="text/css">"
        
    "</style>


</head></html> 

Disclaimers

None.

 

 

Tag page
You must login to post a comment.