commit ae507a6ca36b06b07c4917e8e422ec253f3f3fbd Author: Jack Phoenix Date: Wed Sep 14 15:26:09 2011 +0000 phase II social tools: VoteNY. Tested and built against MW 1.16.0. Can be used without the core social tools package (SocialProfile extension). diff --git a/SpecialTopRatings.php b/SpecialTopRatings.php new file mode 100644 index 0000000..008892d --- /dev/null +++ b/SpecialTopRatings.php @@ -0,0 +1,185 @@ +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 .= ''; + } + + // 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 .= '
' . + $sk->link( + $titleObj, + $titleObj->getPrefixedText() // prefixed, so that the namespace shows! + ) . wfMsg( 'word-separator' ) . // i18n overkill? ya betcha... + wfMsg( 'parentheses', $ratings[$pageId] ) . + '
'; + + $id = mt_rand(); // AFAIK these IDs are and originally were totally random... + $output .= "
" . + $vote->displayStars( + $id, + self::getAverageRatingForPage( $pageId ), + false + ) . '
'; + $output .= "
" . + $vote->displayScore() . + '
'; + } + } 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; + } +} \ No newline at end of file diff --git a/Vote.css b/Vote.css new file mode 100644 index 0000000..802b9c3 --- /dev/null +++ b/Vote.css @@ -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; +} \ No newline at end of file diff --git a/Vote.i18n.php b/Vote.i18n.php new file mode 100644 index 0000000..5f50b36 --- /dev/null +++ b/Vote.i18n.php @@ -0,0 +1,84 @@ + + * @author David Pean + */ +$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 + */ +$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 + */ +$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', +); \ No newline at end of file diff --git a/Vote.js b/Vote.js new file mode 100644 index 0000000..b23e7c2 --- /dev/null +++ b/Vote.js @@ -0,0 +1,136 @@ +/** + * JavaScript functions for Vote extension + * + * @file + * @ingroup Extensions + * @author Jack Phoenix + * @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 = + "" + unvoteMessage + ''; + } ); + }, + + /** + * 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 = + '' + voteMessage + ''; + } ); + }, + + /** + * 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; + } +}; \ No newline at end of file diff --git a/Vote.php b/Vote.php new file mode 100644 index 0000000..fc0206b --- /dev/null +++ b/Vote.php @@ -0,0 +1,75 @@ + tag + * + * @file + * @ingroup Extensions + * @version 2.3.3 + * @author Aaron Wright + * @author David Pean + * @author Jack Phoenix + * @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 <vote> 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 +); \ No newline at end of file diff --git a/VoteClass.php b/VoteClass.php new file mode 100644 index 0000000..725f42e --- /dev/null +++ b/VoteClass.php @@ -0,0 +1,355 @@ +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 = "
PageID},'{$this->votekey}')\">"; + $output .= '' . $this->count() . ''; + $output .= '
'; + $output .= '
'; + + 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 .= '' . + wfMsg( 'vote-link' ) . ''; + } else { + if( !wfReadOnly() ) { + if( $voted == false ) { + $output .= "PageID},'{$this->votekey}')\">" . + wfMsg( 'vote-link' ) . ''; + } else { + $output .= "PageID}', '{$this->votekey}')\">" . + wfMsg( 'vote-unvote-link' ) . ''; + } + } + } + $output .= '
'; + + 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 = '
'; + $output .= '
'; + $output .= '
' . $overall_rating . '
'; + $output .= '
'; + $output .= '
'; + $output .= $this->displayStars( $id, $display_stars_rating, $voted ); + $count = $this->count(); + if( $count ) { + $output .= ' (' . + wfMsgExt( 'vote-votes', 'parsemag', $count ) . ')'; + } + $already_voted = $this->UserAlreadyVoted(); + if( $already_voted && $wgUser->isLoggedIn() ) { + $output .= '
' . + wfMsgExt( 'vote-gave-this', 'parsemag', $already_voted ) . + "
+ PageID},'{$this->votekey}','{$id}')\">(" + . wfMsg( 'vote-remove' ) . + ')'; + } + $output .= '
+
+
'; + + $output .= '
'; + 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 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 .= "= $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', '' . $this->getAverageVote() . '' ) . + ' (' . wfMsgExt( 'vote-ratings', 'parsemag', $count ) . ')'; + } + +} \ No newline at end of file diff --git a/VoteHooks.php b/VoteHooks.php new file mode 100644 index 0000000..3af1bd9 --- /dev/null +++ b/VoteHooks.php @@ -0,0 +1,189 @@ + 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; + } +} \ No newline at end of file diff --git a/Vote_AjaxFunctions.php b/Vote_AjaxFunctions.php new file mode 100644 index 0000000..af7a499 --- /dev/null +++ b/Vote_AjaxFunctions.php @@ -0,0 +1,88 @@ +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(); +} \ No newline at end of file diff --git a/images/star_half.gif b/images/star_half.gif new file mode 100644 index 0000000..c243407 Binary files /dev/null and b/images/star_half.gif differ diff --git a/images/star_off.gif b/images/star_off.gif new file mode 100644 index 0000000..4da3e13 Binary files /dev/null and b/images/star_off.gif differ diff --git a/images/star_on.gif b/images/star_on.gif new file mode 100644 index 0000000..813cee8 Binary files /dev/null and b/images/star_on.gif differ diff --git a/images/star_voted.gif b/images/star_voted.gif new file mode 100644 index 0000000..dbbc91b Binary files /dev/null and b/images/star_voted.gif differ diff --git a/vote.sql b/vote.sql new file mode 100644 index 0000000..de0dea1 --- /dev/null +++ b/vote.sql @@ -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); \ No newline at end of file