phase II social tools: VoteNY. Tested and built against MW 1.16.0. Can be used without the core social tools package (SocialProfile extension).

This commit is contained in:
Jack Phoenix 2011-09-14 15:26:09 +00:00
commit ae507a6ca3
13 changed files with 1196 additions and 0 deletions

185
SpecialTopRatings.php Normal file
View File

@ -0,0 +1,185 @@
<?php
/**
* A special page to display the highest rated pages on the wiki.
*
* This special page supports filtering by category and namespace, so
* {{Special:TopRatings/Adventure Games/0/10}} will show 10 ratings where the
* pages are in the "Adventure Games" category and the pages are in the main
* (0) namespace.
*
* @file
* @ingroup Extensions
* @date 21 August 2011
* @license To the extent that it is possible, this code is in the public domain
*/
class SpecialTopRatings extends IncludableSpecialPage {
/**
* Constructor -- set up the new special page
*/
public function __construct() {
parent::__construct( 'TopRatings' );
}
/**
* Show the special page
*
* @param $par Mixed: parameter passed to the special page or null
*/
public function execute( $par ) {
global $wgOut, $wgScriptPath, $wgUser;
// Set the page title, robot policies, etc.
$this->setHeaders();
$categoryName = $namespace = '';
// Parse the parameters passed to the special page
// Make sure that the limit parameter passed to the special page is
// an integer and that it's less than 100 (performance!)
if ( isset( $par ) && is_numeric( $par ) && $par < 100 ) {
$limit = intval( $par );
} elseif ( isset( $par ) && !is_numeric( $par ) ) {
// $par is a string...assume that we can explode() it
$exploded = explode( '/', $par );
$categoryName = $exploded[0];
$namespace = ( isset( $exploded[1] ) ? intval( $exploded[1] ) : $namespace );
$limit = ( isset( $exploded[2] ) ? intval( $exploded[2] ) : 50 );
} else {
$limit = 50;
}
// Add JS -- needed so that users can vote on this page and so that
// their browsers' consoles won't be filled with JS errors ;-)
$wgOut->addScriptFile( $wgScriptPath . '/extensions/VoteNY/Vote.js' );
$ratings = array();
$output = '';
$sk = $wgUser->getSkin();
$dbr = wfGetDB( DB_SLAVE );
$tables = $where = $joinConds = array();
$whatToSelect = array( 'DISTINCT vote_page_id' );
// By default we have no category and no namespace
$tables = array( 'Vote' );
$where = array( 'vote_page_id <> 0' );
// isset(), because 0 is a totally valid NS
if ( !empty( $categoryName ) && isset( $namespace ) ) {
$tables = array( 'Vote', 'page', 'categorylinks' );
$where = array(
'vote_page_id <> 0',
'cl_to' => str_replace( ' ', '_', $categoryName ),
'page_namespace' => $namespace
);
$joinConds = array(
'categorylinks' => array( 'INNER JOIN', 'cl_from = page_id' ),
'page' => array( 'INNER JOIN', 'page_id = vote_page_id' )
);
}
// Perform the SQL query with the given conditions; the basic idea is
// that we get $limit (however, 100 or less) unique page IDs from the
// Vote table. If a category and a namespace have been given, we also
// do an INNER JOIN with page and categorylinks table to get the
// correct data.
$res = $dbr->select(
$tables,
$whatToSelect,
$where,
__METHOD__,
array( 'LIMIT' => intval( $limit ) ),
$joinConds
);
foreach ( $res as $row ) {
// Add the results to the $ratings array and get the amount of
// votes the given page ID has
// For example: $ratings[1] = 11 = page with the page ID 1 has 11
// votes
$ratings[$row->vote_page_id] = (int)$dbr->selectField(
'Vote',
'SUM(vote_value)',
array( 'vote_page_id' => $row->vote_page_id ),
__METHOD__
);
}
// If we have some ratings, start building HTML output
if ( !empty( $ratings ) ) {
/* XXX dirrrrrrty hack! because when we include this page, the JS
is not included, but we want things to work still */
if ( $this->including() ) {
$output .= '<script type="text/javascript" src="' .
$wgScriptPath . '/extensions/VoteNY/Vote.js"></script>';
}
// yes, array_keys() is needed
foreach ( array_keys( $ratings ) as $discardThis => $pageId ) {
$titleObj = Title::newFromId( $pageId );
if ( !( $titleObj instanceof Title ) ) {
continue;
}
$vote = new VoteStars( $pageId );
$output .= '<div class="user-list-rating">' .
$sk->link(
$titleObj,
$titleObj->getPrefixedText() // prefixed, so that the namespace shows!
) . wfMsg( 'word-separator' ) . // i18n overkill? ya betcha...
wfMsg( 'parentheses', $ratings[$pageId] ) .
'</div>';
$id = mt_rand(); // AFAIK these IDs are and originally were totally random...
$output .= "<div id=\"rating_stars_{$id}\">" .
$vote->displayStars(
$id,
self::getAverageRatingForPage( $pageId ),
false
) . '</div>';
$output .= "<div id=\"rating_{$id}\" class=\"rating-total\">" .
$vote->displayScore() .
'</div>';
}
} else {
// Nothing? Well, display an informative error message rather than
// a blank page or somesuch.
$output .= wfMsg( 'topratings-no-pages' );
}
// Output everything!
$wgOut->addHTML( $output );
}
/**
* Static version of Vote::getAverageVote().
*
* @param $pageId Integer: ID of the page for which we want to get the avg.
* rating
* @return Integer: average vote for the given page (ID)
*/
public static function getAverageRatingForPage( $pageId ) {
global $wgMemc;
$key = wfMemcKey( 'vote', 'avg', $pageId );
$data = $wgMemc->get( $key );
$voteAvg = 0;
if( $data ) {
wfDebug( "Loading vote avg for page {$pageId} from cache (TopRatings)\n" );
$voteAvg = $data;
} else {
$dbr = wfGetDB( DB_SLAVE );
$voteAvg = (int)$dbr->selectField(
'Vote',
'AVG(vote_value) AS VoteAvg',
array( 'vote_page_id' => $pageId ),
__METHOD__
);
$wgMemc->set( $key, $voteAvg );
}
return $voteAvg;
}
}

63
Vote.css Normal file
View File

@ -0,0 +1,63 @@
/* CSS for Vote extension */
.vote-box {
background-color: #68BD46;
height: 30px;
padding: 13px 0px 0px;
text-align: center;
width: 43px;
}
.vote-number {
color: #FFF;
font-size: 16px;
font-weight: bold;
}
.vote-action {
text-align: center;
width: 43px;
}
.vote-action a {
font-weight: bold;
font-size: 11px;
text-decoration: none;
}
.rating-score {
background-color: #68BD46;
color: #FFF;
float: left;
font-size: 14px;
font-weight: bold;
padding: 1px 8px 0px;
margin: 1px 7px 0px 0px;
text-align: center;
}
.ratings-top {
position: absolute;
top: 37px !important;
right: 0px !important;
width: 100%;
}
.rating-section img {
vertical-align: text-bottom;
}
.rating-voted {
color: #666666;
line-height: 10px;
font-size: 9px;
position: absolute;
right: 0px;
}
/* Styling for the (n votes) after rating box/stars */
.rating-total {
color: #666;
font-weight: bold;
font-size: 11px;
margin: 3px 0px 0px 0px;
}

84
Vote.i18n.php Normal file
View File

@ -0,0 +1,84 @@
<?php
/**
* Internationalization file for the Vote extension.
*
* @file
* @ingroup Extensions
*/
$messages = array();
/** English
* @author Aaron Wright <aaron.wright@gmail.com>
* @author David Pean <david.pean@gmail.com>
*/
$messages['en'] = array(
'vote-link' => 'Vote',
'vote-unvote-link' => 'unvote',
'vote-community-score' => 'community score: $1',
'vote-ratings' => '{{PLURAL:$1|one rating|$1 ratings}}',
'vote-remove' => 'remove',
'vote-gave-this' => 'you gave this a $1',
'vote-votes' => '{{PLURAL:$1|one vote|$1 votes}}',
// Special:TopRatings
'topratings' => 'Top rated pages',
'topratings-no-pages' => 'No top rated pages.',
// For Special:ListGroupRights
'right-vote' => 'Vote pages',
);
/** Finnish (Suomi)
* @author Jack Phoenix <jack@countervandalism.net>
*/
$messages['fi'] = array(
'vote-link' => 'Äänestä',
'vote-unvote-link' => 'poista ääni',
'vote-community-score' => 'yhteisön antama pistemäärä: $1',
'vote-ratings' => '{{PLURAL:$1|yksi arvostelu|$1 arvostelua}}',
'vote-remove' => 'poista',
'vote-gave-this' => 'annoit tälle {{PLURAL:$1|yhden tähden|$1 tähteä}}',
'vote-votes' => '{{PLURAL:$1|yksi ääni|$1 ääntä}}',
'topratings' => 'Huippusivut',
'topratings-no-pages' => 'Ei huippusivuja.',
'right-vote' => 'Äänestää sivuja',
);
/** French (Français)
* @author Jack Phoenix <jack@countervandalism.net>
*/
$messages['fr'] = array(
'vote-link' => 'Voter',
'vote-unvote-link' => 'supprimer vote',
'vote-remove' => 'supprimer',
'vote-votes' => '{{PLURAL:$1|un vote|$1 votes}}',
'right-vote' => 'Voter pages',
);
/** Dutch (Nederlands)
* @author Mitchel Corstjens
*/
$messages['nl'] = array(
'vote-link' => 'Stem',
'vote-unvote-link' => 'stem terugtrekken',
'vote-community-score' => 'gemeenschap score: $1',
'vote-remove' => 'verwijder',
'vote-gave-this' => 'je gaf dit een $1',
'vote-votes' => '{{PLURAL:$1|een stem|$1 stemmen}}',
'topratings' => 'Meest gewaardeerde pagina\'s',
'topratings-no-pages' => 'Er zijn nog geen meest gewaardeerde pagina\'s',
'right-vote' => 'Stem paginas',
);
/** Polish (Polski)
* @author Misiek95
*/
$messages['pl'] = array(
'vote-link' => 'Głosuj',
'vote-unvote-link' => 'Anuluj',
'vote-community-score' => 'Wynik wśród społeczności: $1',
'vote-ratings' => '{{PLURAL:$1|1 głos|$1 głosy|$1 głosów}}',
'vote-remove' => 'usuń',
'vote-gave-this' => 'Oceniłeś to na $1',
'vote-votes' => '{{PLURAL:$1|1 głos|$1 głosy|$1 głosów}}',
'right-vote' => 'Udział w głosowaniach',
);

136
Vote.js Normal file
View File

@ -0,0 +1,136 @@
/**
* JavaScript functions for Vote extension
*
* @file
* @ingroup Extensions
* @author Jack Phoenix <jack@countervandalism.net>
* @date 19 June 2011
*/
var VoteNY = {
MaxRating: 5,
clearRatingTimer: '',
voted_new: [],
id: 0,
last_id: 0,
imagePath: wgScriptPath + '/extensions/VoteNY/images/',
/**
* Called when voting through the green square voting box
* @param TheVote
* @param PageID Integer: internal ID number of the current article
* @param mk Mixed: random token
*/
clickVote: function( TheVote, PageID, mk ) {
sajax_request_type = 'POST';
sajax_do_call( 'wfVoteClick', [ TheVote, PageID, mk ], function( request ) {
document.getElementById( 'votebox' ).style.cursor = 'default';
document.getElementById( 'PollVotes' ).innerHTML = request.responseText;
var unvoteMessage;
if ( typeof( mediaWiki ) == 'undefined' ) {
unvoteMessage = _UNVOTE_LINK;
} else {
unvoteMessage = mediaWiki.msg( 'vote-unvote-link' );
}
document.getElementById( 'Answer' ).innerHTML =
"<a href=javascript:VoteNY.unVote(" + PageID + ",'" + mk +
"')>" + unvoteMessage + '</a>';
} );
},
/**
* Called when removing your vote through the green square voting box
* @param PageID Integer: internal ID number of the current article
* @param mk Mixed: random token
*/
unVote: function( PageID, mk ) {
sajax_request_type = 'POST';
sajax_do_call( 'wfVoteDelete', [ PageID, mk ], function( request ) {
document.getElementById( 'votebox' ).style.cursor = 'pointer';
document.getElementById( 'PollVotes' ).innerHTML = request.responseText;
var voteMessage;
if ( typeof( mediaWiki ) == 'undefined' ) {
voteMessage = _VOTE_LINK;
} else {
voteMessage = mediaWiki.msg( 'vote-link' );
}
document.getElementById( 'Answer' ).innerHTML =
'<a href=javascript:VoteNY.clickVote(1,' + PageID + ',"' + mk +
'")>' + voteMessage + '</a>';
} );
},
/**
* Called when adding a vote after a user has clicked the yellow voting stars
* @param PageID Integer: internal ID number of the current article
* @param mk Mixed: random token
* @param id Integer: ID of the current rating star
* @param action Integer: controls which AJAX function will be called
*/
clickVoteStars: function( TheVote, PageID, mk, id, action ) {
VoteNY.voted_new[id] = TheVote;
var rsfun;
if( action == 3 ) {
rsfun = 'wfVoteStars';
}
if( action == 5 ) {
rsfun = 'wfVoteStarsMulti';
}
var resultElement = document.getElementById( 'rating_' + id );
sajax_request_type = 'POST';
sajax_do_call( rsfun, [ TheVote, PageID, mk ], resultElement );
},
/**
* Called when removing your vote through the yellow voting stars
* @param PageID Integer: internal ID number of the current article
* @param mk Mixed: random token
* @param id Integer: ID of the current rating star
*/
unVoteStars: function( PageID, mk, id ) {
var resultElement = document.getElementById( 'rating_' + id );
sajax_request_type = 'POST';
sajax_do_call( 'wfVoteStarsDelete', [ PageID, mk ], resultElement );
},
startClearRating: function( id, rating, voted ) {
VoteNY.clearRatingTimer = setTimeout(
"VoteNY.clearRating('" + id + "',0," + rating + ',' + voted + ')',
200
);
},
clearRating: function( id, num, prev_rating, voted ) {
if( VoteNY.voted_new[id] ) {
voted = VoteNY.voted_new[id];
}
for( var x = 1; x <= VoteNY.MaxRating; x++ ) {
var star_on, old_rating;
if( voted ) {
star_on = 'voted';
old_rating = voted;
} else {
star_on = 'on';
old_rating = prev_rating;
}
var ratingElement = document.getElementById( 'rating_' + id + '_' + x );
if( !num && old_rating >= x ) {
ratingElement.src = VoteNY.imagePath + 'star_' + star_on + '.gif';
} else {
ratingElement.src = VoteNY.imagePath + 'star_off.gif';
}
}
},
updateRating: function( id, num, prev_rating ) {
if( VoteNY.clearRatingTimer && VoteNY.last_id == id ) {
clearTimeout( VoteNY.clearRatingTimer );
}
VoteNY.clearRating( id, num, prev_rating );
for( var x = 1; x <= num; x++ ) {
document.getElementById( 'rating_' + id + '_' + x ).src = VoteNY.imagePath + 'star_voted.gif';
}
VoteNY.last_id = id;
}
};

75
Vote.php Normal file
View File

@ -0,0 +1,75 @@
<?php
/**
* Vote extension - JavaScript-based voting with the <vote> tag
*
* @file
* @ingroup Extensions
* @version 2.3.3
* @author Aaron Wright <aaron.wright@gmail.com>
* @author David Pean <david.pean@gmail.com>
* @author Jack Phoenix <jack@countervandalism.net>
* @link http://www.mediawiki.org/wiki/Extension:VoteNY Documentation
* @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
*/
/**
* Protect against register_globals vulnerabilities.
* This line must be present before any global variable is referenced.
*/
if ( !defined( 'MEDIAWIKI' ) ) {
die( "This is not a valid entry point.\n" );
}
// Extension credits that show up on Special:Version
$wgExtensionCredits['parserhook'][] = array(
'name' => 'Vote',
'version' => '2.3.3',
'author' => array( 'Aaron Wright', 'David Pean', 'Jack Phoenix' ),
'description' => 'JavaScript-based voting with the <tt>&lt;vote&gt;</tt> tag',
'url' => 'http://www.mediawiki.org/wiki/Extension:VoteNY'
);
// Path to Vote extension files
$wgVoteDirectory = "$IP/extensions/VoteNY";
// New user right
$wgAvailableRights[] = 'vote';
$wgGroupPermissions['*']['vote'] = false; // Anonymous users cannot vote
$wgGroupPermissions['user']['vote'] = true; // Registered users can vote
// AJAX functions needed by this extension
require_once( 'Vote_AjaxFunctions.php' );
// Autoload classes and set up i18n
$dir = dirname( __FILE__ ) . '/';
$wgExtensionMessagesFiles['Vote'] = $dir . 'Vote.i18n.php';
$wgAutoloadClasses['Vote'] = $dir . 'VoteClass.php';
$wgAutoloadClasses['VoteStars'] = $dir . 'VoteClass.php';
// Set up the new special page, Special:TopRatings, which shows top rated pages
// based on given criteria
$wgAutoloadClasses['SpecialTopRatings'] = $dir . 'SpecialTopRatings.php';
$wgSpecialPages['TopRatings'] = 'SpecialTopRatings';
// Hooked functions
$wgAutoloadClasses['VoteHooks'] = $dir . 'VoteHooks.php';
$wgHooks['ParserFirstCallInit'][] = 'VoteHooks::registerParserHook';
$wgHooks['MakeGlobalVariablesScript'][] = 'VoteHooks::addJSGlobalVariables';
$wgHooks['RenameUserSQL'][] = 'VoteHooks::onUserRename';
// Translations for {{NUMBEROFVOTES}}
//$wgExtensionMessagesFiles['NumberOfVotes'] = $dir . 'Vote.i18n.magic.php';
$wgHooks['LanguageGetMagic'][] = 'VoteHooks::setUpMagicWord';
$wgHooks['ParserGetVariableValueSwitch'][] = 'VoteHooks::assignValueToMagicWord';
$wgHooks['MagicWordwgVariableIDs'][] = 'VoteHooks::registerVariableId';
$wgHooks['LoadExtensionSchemaUpdates'][] = 'VoteHooks::addTable';
// ResourceLoader support for MediaWiki 1.17+
$wgResourceModules['ext.voteNY'] = array(
'styles' => 'Vote.css',
'scripts' => 'Vote.js',
'messages' => array( 'vote-link', 'vote-unvote-link' ),
'localBasePath' => dirname( __FILE__ ),
'remoteExtPath' => 'VoteNY',
'position' => 'top' // available since r85616
);

355
VoteClass.php Normal file
View File

@ -0,0 +1,355 @@
<?php
/**
* Vote class - class for handling vote-related functions (counting
* the average score of a given page, inserting/updating/removing a vote etc.)
*
* @file
* @ingroup Extensions
*/
class Vote {
var $PageID = 0;
var $Userid = 0;
var $Username = null;
/**
* Constructor
* @param $pageID Integer: article ID number
*/
public function __construct( $pageID ) {
global $wgUser;
$this->PageID = $pageID;
$this->Username = $wgUser->getName();
$this->Userid = $wgUser->getID();
}
/**
* Counts all votes, fetching the data from memcached if available
* or from the database if memcached isn't available
* @return Integer: amount of votes
*/
function count() {
global $wgMemc;
$key = wfMemcKey( 'vote', 'count', $this->PageID );
$data = $wgMemc->get( $key );
// Try cache
if( $data ) {
wfDebug( "Loading vote count for page {$this->PageID} from cache\n" );
$vote_count = $data;
} else {
$dbr = wfGetDB( DB_SLAVE );
$vote_count = 0;
$res = $dbr->select(
'Vote',
'COUNT(*) AS VoteCount',
array( 'vote_page_id' => $this->PageID ),
__METHOD__
);
$row = $dbr->fetchObject( $res );
if( $row ) {
$vote_count = $row->VoteCount;
}
$wgMemc->set( $key, $vote_count );
}
return $vote_count;
}
/**
* Gets the average score of all votes
* @return Integer: formatted average number of votes (something like 3.50)
*/
function getAverageVote() {
global $wgMemc;
$key = wfMemcKey( 'vote', 'avg', $this->PageID );
$data = $wgMemc->get( $key );
$voteAvg = 0;
if( $data ) {
wfDebug( "Loading vote avg for page {$this->PageID} from cache\n" );
$voteAvg = $data;
} else {
$dbr = wfGetDB( DB_SLAVE );
$res = $dbr->select(
'Vote',
'AVG(vote_value) AS VoteAvg',
array( 'vote_page_id' => $this->PageID ),
__METHOD__
);
$row = $dbr->fetchObject( $res );
if( $row ) {
$voteAvg = $row->VoteAvg;
}
$wgMemc->set( $key, $voteAvg );
}
return number_format( $voteAvg, 2 );
}
/**
* Clear caches - memcached, parser cache and Squid cache
*/
function clearCache() {
global $wgUser, $wgMemc;
// Kill internal cache
$wgMemc->delete( wfMemcKey( 'vote', 'count', $this->PageID ) );
$wgMemc->delete( wfMemcKey( 'vote', 'avg', $this->PageID ) );
// Purge squid
$page_title = Title::newFromID( $this->PageID );
if( is_object( $page_title ) ) {
$page_title->invalidateCache();
$page_title->purgeSquid();
// Kill parser cache
$article = new Article( $page_title );
$parserCache =& ParserCache::singleton();
$parser_key = $parserCache->getKey( $article, $wgUser );
$wgMemc->delete( $parser_key );
}
}
/**
* Delete the user's vote from the DB if s/he wants to remove his/her vote
*/
function delete() {
$dbw = wfGetDB( DB_MASTER );
$dbw->delete(
'Vote',
array(
'vote_page_id' => $this->PageID,
'username' => $this->Username
),
__METHOD__
);
$dbw->commit();
$this->clearCache();
// Update social statistics if SocialProfile extension is enabled
if( class_exists( 'UserStatsTrack' ) ) {
$stats = new UserStatsTrack( $this->Userid, $this->Username );
$stats->decStatField( 'vote' );
}
}
/**
* Inserts a new vote into the Vote database table
* @param $voteValue
*/
function insert( $voteValue ) {
$dbw = wfGetDB( DB_MASTER );
wfSuppressWarnings(); // E_STRICT whining
$voteDate = date( 'Y-m-d H:i:s' );
wfRestoreWarnings();
if( $this->UserAlreadyVoted() == false ) {
$dbw->insert(
'Vote',
array(
'username' => $this->Username,
'vote_user_id' => $this->Userid,
'vote_page_id' => $this->PageID,
'vote_value' => $voteValue,
'vote_date' => $voteDate,
'vote_ip' => wfGetIP()
),
__METHOD__
);
$dbw->commit();
$this->clearCache();
// Update social statistics if SocialProfile extension is enabled
if( class_exists( 'UserStatsTrack' ) ) {
$stats = new UserStatsTrack( $this->Userid, $this->Username );
$stats->incStatField( 'vote' );
}
}
}
/**
* Checks if a user has already voted
* @return Boolean: false if s/he hasn't, otherwise returns the value of
* 'vote_value' column from Vote DB table
*/
function UserAlreadyVoted() {
$dbr = wfGetDB( DB_SLAVE );
$s = $dbr->selectRow(
'Vote',
array( 'vote_value' ),
array(
'vote_page_id' => $this->PageID,
'username' => $this->Username
),
__METHOD__
);
if( $s === false ) {
return false;
} else {
return $s->vote_value;
}
}
/**
* Displays the green voting box
* @return Mixed: HTML output
*/
function display() {
global $wgUser;
$this->votekey = md5( $this->PageID . 'pants' . $this->Username );
$voted = $this->UserAlreadyVoted();
$make_vote_box_clickable = '';
if( $voted == false ) {
$make_vote_box_clickable = ' vote-clickable';
}
$output = "<div class=\"vote-box{$make_vote_box_clickable}\" id=\"votebox\" onclick=\"VoteNY.clickVote(1,{$this->PageID},'{$this->votekey}')\">";
$output .= '<span id="PollVotes" class="vote-number">' . $this->count() . '</span>';
$output .= '</div>';
$output .= '<div id="Answer" class="vote-action">';
if ( !$wgUser->isAllowed( 'vote' ) ) {
// @todo FIXME: this is horrible. If we don't have enough
// permissions to vote, we should tell the end-user /that/,
// not require them to log in!
$login = SpecialPage::getTitleFor( 'Userlogin' );
$output .= '<a class="votebutton" href="' .
$login->escapeFullURL() . '" rel="nofollow">' .
wfMsg( 'vote-link' ) . '</a>';
} else {
if( !wfReadOnly() ) {
if( $voted == false ) {
$output .= "<a href=\"javascript:VoteNY.clickVote(1,{$this->PageID},'{$this->votekey}')\">" .
wfMsg( 'vote-link' ) . '</a>';
} else {
$output .= "<a href=\"javascript:VoteNY.unVote('{$this->PageID}', '{$this->votekey}')\">" .
wfMsg( 'vote-unvote-link' ) . '</a>';
}
}
}
$output .= '</div>';
return $output;
}
}
/**
* Class for generating star rating stars.
*/
class VoteStars extends Vote {
var $maxRating = 5;
/**
* Displays voting stars
* @param $voted Boolean: false by default
* @return Mixed: HTML output
*/
function display( $voted = false ) {
global $wgUser;
$overall_rating = $this->getAverageVote();
if( $voted ) {
$display_stars_rating = $voted;
} else {
$display_stars_rating = $this->getAverageVote();
}
$this->votekey = md5( $this->PageID . 'pants' . $this->Username );
$id = '';
// Should probably be $this->PageID or something?
// 'cause we define $id just above as an empty string...duh
$output = '<div id="rating_' . $id . '">';
$output .= '<div class="rating-score">';
$output .= '<div class="voteboxrate">' . $overall_rating . '</div>';
$output .= '</div>';
$output .= '<div class="rating-section">';
$output .= $this->displayStars( $id, $display_stars_rating, $voted );
$count = $this->count();
if( $count ) {
$output .= ' <span class="rating-total">(' .
wfMsgExt( 'vote-votes', 'parsemag', $count ) . ')</span>';
}
$already_voted = $this->UserAlreadyVoted();
if( $already_voted && $wgUser->isLoggedIn() ) {
$output .= '<div class="rating-voted">' .
wfMsgExt( 'vote-gave-this', 'parsemag', $already_voted ) .
" </div>
<a href=\"javascript:VoteNY.unVoteStars({$this->PageID},'{$this->votekey}','{$id}')\">("
. wfMsg( 'vote-remove' ) .
')</a>';
}
$output .= '</div>
<div class="rating-clear">
</div>';
$output .= '</div>';
return $output;
}
/**
* Displays the actual star images, depending on the state of the user's mouse
* @param $id Integer: ID of the rating (div) element
* @param $rating Integer: average rating
* @param $voted Integer
* @return Mixed: generated <img> tag
*/
function displayStars( $id, $rating, $voted ) {
global $wgScriptPath;
if( !$rating ) {
$rating = 0;
}
$this->votekey = md5( $this->PageID . 'pants' . $this->Username );
if( !$voted ) {
$voted = 0;
}
$output = '';
for( $x = 1; $x <= $this->maxRating; $x++ ) {
if( !$id ) {
$action = 3;
} else {
$action = 5;
}
$onclick = "VoteNY.clickVoteStars({$x},{$this->PageID},'{$this->votekey}','{$id}',$action);";
$onmouseover = "VoteNY.updateRating('{$id}',{$x},{$rating});";
$onmouseout = "VoteNY.startClearRating('{$id}','{$rating}',{$voted});";
$output .= "<img onclick=\"javascript:{$onclick}\" onmouseover=\"javascript:{$onmouseover}\" onmouseout=\"javascript:{$onmouseout}\" id=\"rating_{$id}_{$x}\" src=\"{$wgScriptPath}/extensions/VoteNY/images/star_";
switch( true ) {
case $rating >= $x:
if( $voted ) {
$output .= 'voted';
} else {
$output .= 'on';
}
break;
case( $rating > 0 && $rating < $x && $rating > ( $x - 1 ) ):
$output .= 'half';
break;
case( $rating < $x ):
$output .= 'off';
break;
}
$output .= '.gif" alt="" />';
}
return $output;
}
/**
* Displays the average score for the current page
* and the total amount of votes.
*/
function displayScore() {
$count = $this->count();
return wfMsg( 'vote-community-score', '<b>' . $this->getAverageVote() . '</b>' ) .
' (' . wfMsgExt( 'vote-ratings', 'parsemag', $count ) . ')';
}
}

189
VoteHooks.php Normal file
View File

@ -0,0 +1,189 @@
<?php
/**
* All hooked functions used by VoteNY extension.
*
* @file
* @ingroup Extensions
*/
class VoteHooks {
/**
* Set up the <vote> parser hook.
*
* @param $parser Parser: instance of Parser
* @return Boolean: true
*/
public static function registerParserHook( &$parser ) {
$parser->setHook( 'vote', array( 'VoteHooks', 'renderVote' ) );
return true;
}
/**
* Callback function for registerParserHook.
*
* @param $input String: user-supplied input, unused
* @param $args Array: user-supplied arguments, unused
* @param $parser Parser: instance of Parser, unused
* @return String: HTML
*/
public static function renderVote( $input, $args, $parser ) {
global $wgOut, $wgTitle, $wgScriptPath;
wfProfileIn( __METHOD__ );
// Disable parser cache (sadly we have to do this, because the caching is
// messing stuff up; we want to show an up-to-date rating instead of old
// or totally wrong rating, i.e. another page's rating...)
$parser->disableCache();
// Add CSS & JS
// In order for us to do this *here* instead of having to do this in
// registerParserHook(), we must've disabled parser cache
if ( defined( 'MW_SUPPORTS_RESOURCE_MODULES' ) ) {
$wgOut->addModules( 'ext.voteNY' );
} else {
$wgOut->addScriptFile( $wgScriptPath . '/extensions/VoteNY/Vote.js' );
$wgOut->addExtensionStyle( $wgScriptPath . '/extensions/VoteNY/Vote.css' );
}
// Define variable - 0 means that we'll get that green voting box by default
$type = 0;
// Determine what kind of a voting gadget the user wants: a box or pretty stars?
if( preg_match( "/^\s*type\s*=\s*(.*)/mi", $input, $matches ) ) {
$type = htmlspecialchars( $matches[1] );
} elseif( !empty( $args['type'] ) ) {
$type = intval( $args['type'] );
}
$articleID = $wgTitle->getArticleID();
switch( $type ) {
case 0:
$vote = new Vote( $articleID );
break;
case 1:
$vote = new VoteStars( $articleID );
break;
default:
$vote = new Vote( $articleID );
}
$output = $vote->display();
wfProfileOut( __METHOD__ );
return $output;
}
/**
* Adds required JS variables to the HTML output.
*
* @param $vars Array: array of pre-existing JS globals
* @return Boolean: true
*/
public static function addJSGlobalVariables( $vars ) {
$vars['_VOTE_LINK'] = wfMsg( 'vote-link' );
$vars['_UNVOTE_LINK'] = wfMsg( 'vote-unvote-link' );
return true;
}
/**
* For the Renameuser extension.
*
* @param $renameUserSQL
* @return Boolean: true
*/
public static function onUserRename( $renameUserSQL ) {
$renameUserSQL->tables['Vote'] = array( 'username', 'vote_user_id' );
return true;
}
/**
* Set up the {{NUMBEROFVOTES}} magic word.
*
* @param $magicWords Array: array of magic words
* @param $langID
* @return Boolean: true
*/
public static function setUpMagicWord( &$magicWords, $langID ) {
// tell MediaWiki that {{NUMBEROFVOTES}} and all case variants found in
// wiki text should be mapped to magic ID 'NUMBEROFVOTES'
// (0 means case-insensitive)
$magicWords['NUMBEROFVOTES'] = array( 0, 'NUMBEROFVOTES' );
return true;
}
/**
* Assign a value to {{NUMBEROFVOTES}}. First we try memcached and if that
* fails, we fetch it directly from the database and cache it for 24 hours.
*
* @param $parser Parser
* @param $cache
* @param $magicWordId String: magic word ID
* @param $ret Integer: return value (number of votes)
* @return Boolean: true
*/
public static function assignValueToMagicWord( &$parser, &$cache, &$magicWordId, &$ret ) {
global $wgMemc;
if ( $magicWordId == 'NUMBEROFVOTES' ) {
$key = wfMemcKey( 'vote', 'magic-word' );
$data = $wgMemc->get( $key );
if ( $data != '' ) {
// We have it in cache? Oh goody, let's just use the cached value!
wfDebugLog(
'VoteNY',
'Got the amount of votes from memcached'
);
// return value
$ret = $data;
} else {
// Not cached → have to fetch it from the database
$dbr = wfGetDB( DB_SLAVE );
$voteCount = (int)$dbr->selectField(
'Vote',
'COUNT(*) AS count',
array(),
__METHOD__
);
wfDebugLog( 'VoteNY', 'Got the amount of votes from DB' );
// Store the count in cache...
// (86400 = seconds in a day)
$wgMemc->set( $key, $voteCount, 86400 );
// ...and return the value to the user
$ret = $voteCount;
}
}
return true;
}
/**
* Register the magic word ID for {{NUMBEROFVOTES}}.
*
* @param $variableIds Array: array of pre-existing variable IDs
* @return Boolean: true
*/
public static function registerVariableId( &$variableIds ) {
$variableIds[] = 'NUMBEROFVOTES';
return true;
}
/**
* Creates the necessary database table when the user runs
* maintenance/update.php.
*
* @param $updater Object: instance of DatabaseUpdater
* @return Boolean: true
*/
public static function addTable( $updater = null ) {
$dir = dirname( __FILE__ );
$file = "$dir/vote.sql";
if ( $updater === null ) {
global $wgExtNewTables;
$wgExtNewTables[] = array( 'Vote', $file );
} else {
$updater->addExtensionUpdate( array( 'addTable', 'Vote', $file, true ) );
}
return true;
}
}

88
Vote_AjaxFunctions.php Normal file
View File

@ -0,0 +1,88 @@
<?php
/**
* AJAX functions used by Vote extension.
*/
$wgAjaxExportList[] = 'wfVoteClick';
function wfVoteClick( $voteValue, $pageId, $mk ) {
global $wgUser;
if ( !$wgUser->isAllowed( 'vote' ) ) {
return '';
}
if( is_numeric( $pageId ) && ( is_numeric( $voteValue ) ) ) {
$vote = new Vote( $pageId );
$vote->insert( $voteValue );
return $vote->count( 1 );
} else {
return 'error';
}
}
$wgAjaxExportList[] = 'wfVoteDelete';
function wfVoteDelete( $pageId, $mk ) {
global $wgUser;
if ( !$wgUser->isAllowed( 'vote' ) ) {
return '';
}
if( is_numeric( $pageId ) ) {
$vote = new Vote( $pageId );
$vote->delete();
return $vote->count( 1 );
} else {
return 'error';
}
}
$wgAjaxExportList[] = 'wfVoteStars';
function wfVoteStars( $voteValue, $pageId, $mk ) {
global $wgUser;
if ( !$wgUser->isAllowed( 'vote' ) ) {
return '';
}
$vote = new VoteStars( $pageId );
if( $vote->UserAlreadyVoted() ) {
$vote->delete();
}
$vote->insert( $voteValue );
return $vote->display( $voteValue );
}
$wgAjaxExportList[] = 'wfVoteStarsMulti';
function wfVoteStarsMulti( $voteValue, $pageId, $mk ) {
global $wgUser;
if ( !$wgUser->isAllowed( 'vote' ) ) {
return '';
}
$vote = new VoteStars( $pageId );
if( $vote->UserAlreadyVoted() ) {
$vote->delete();
}
$vote->insert( $voteValue );
return $vote->displayScore();
}
$wgAjaxExportList[] = 'wfVoteStarsDelete';
function wfVoteStarsDelete( $pageId ) {
global $wgUser;
if ( !$wgUser->isAllowed( 'vote' ) ) {
return '';
}
$vote = new VoteStars( $pageId );
$vote->delete();
return $vote->display();
}

BIN
images/star_half.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 B

BIN
images/star_off.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 631 B

BIN
images/star_on.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 627 B

BIN
images/star_voted.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 612 B

21
vote.sql Normal file
View File

@ -0,0 +1,21 @@
CREATE TABLE /*_*/Vote (
-- Internal ID to identify between different vote tags on different pages
`vote_id` int(11) NOT NULL auto_increment PRIMARY KEY,
-- Username (if any) of the person who voted
`username` varchar(255) NOT NULL default '0',
-- User ID of the person who voted
`vote_user_id` int(11) NOT NULL default '0',
-- ID of the page where the vote tag is in
`vote_page_id` int(11) NOT NULL default '0',
-- Value of the vote (ranging from 1 to 5)
`vote_value` char(1) character set latin1 collate latin1_bin NOT NULL default '',
-- Timestamp when the vote was cast
`vote_date` datetime NOT NULL default '0000-00-00 00:00:00',
-- IP address of the user who voted
`vote_ip` varchar(45) NOT NULL default ''
) /*$wgDBTableOptions*/;
CREATE INDEX vote_page_id_index ON /*_*/Vote (vote_page_id);
CREATE INDEX valueidx ON /*_*/Vote (vote_value);
CREATE INDEX usernameidx ON /*_*/Vote (username);
CREATE INDEX vote_date ON /*_*/Vote (vote_date);