Index: trunk/extensions/CommunityVoice/CLI/Initialize.php |
— | — | @@ -0,0 +1,20 @@ |
| 2 | +<?php |
| 3 | + |
| 4 | +require_once dirname( dirname( dirname( dirname( __FILE__ ) ) ) ) . |
| 5 | + "/maintenance/commandLine.inc"; |
| 6 | + |
| 7 | +if ( isset( $options['help'] ) ) { |
| 8 | + echo "Rebuilds database structure for CommunityVoice.\n"; |
| 9 | + echo "Usage:\n"; |
| 10 | + echo "\tphp extensions/CommunityVoice/CLI/Initialize.php --confirm=yes \n"; |
| 11 | +} else { |
| 12 | + if ( isset( $options['confirm'] ) && $options['confirm'] == 'yes' ) { |
| 13 | + echo "Rebuilding database structure for CommunityVoice...\n"; |
| 14 | + // Get a connection |
| 15 | + $dbw = wfGetDB( DB_MASTER ); |
| 16 | + // Runs initialization |
| 17 | + $dbw->sourceFile( dirname( dirname( __FILE__ ) ) . '/CommunityVoice.sql' ); |
| 18 | + } else { |
| 19 | + echo "Nothing was changed. Run with --help for usage information.\n"; |
| 20 | + } |
| 21 | +} |
\ No newline at end of file |
Index: trunk/extensions/CommunityVoice/CommunityVoice.sql |
— | — | @@ -0,0 +1,33 @@ |
| 2 | +-- |
| 3 | +-- SQL for CommunityVoice Extension |
| 4 | +-- |
| 5 | +-- Table for ratings |
| 6 | +DROP TABLE IF EXISTS /*$wgDBprefix*/cv_ratings_votes; |
| 7 | +CREATE TABLE /*$wgDBPrefix*/cv_ratings_votes ( |
| 8 | + -- Category of item being rated |
| 9 | + vot_category VARBINARY(255) NOT NULL default '', |
| 10 | + -- Title of item being rated |
| 11 | + vot_title VARBINARY(255) NOT NULL default '', |
| 12 | + -- User who made the rating, 0 for anons (however it shoudn't be allowed) |
| 13 | + vot_user INTEGER NOT NULL default 0, |
| 14 | + -- Value of rating |
| 15 | + vot_rating INTEGER NOT NULL default 0, |
| 16 | + -- |
| 17 | + INDEX vot_category_title ( vot_category, vot_title ), |
| 18 | + INDEX vot_category_title_user ( vot_category, vot_title, vot_user ), |
| 19 | + INDEX vot_category_title_rating ( vot_category, vot_title, vot_rating ) |
| 20 | +) /*$wgDBTableOptions*/; |
| 21 | +-- |
| 22 | +-- Table for articles which include ratings |
| 23 | +DROP TABLE IF EXISTS /*$wgDBprefix*/cv_ratings_usage; |
| 24 | +CREATE TABLE /*$wgDBPrefix*/cv_ratings_usage ( |
| 25 | + -- Category of item being rated |
| 26 | + usg_category VARBINARY(255) NOT NULL default '', |
| 27 | + -- Title of item being rated |
| 28 | + usg_title VARBINARY(255) NOT NULL default '', |
| 29 | + -- Title of article which includes the rating |
| 30 | + usg_article VARBINARY(255) NOT NULL default '', |
| 31 | + -- |
| 32 | + INDEX rat_category_title ( usg_category, usg_title ), |
| 33 | + INDEX rat_category_title_article ( usg_category, usg_title, usg_article ) |
| 34 | +) /*$wgDBTableOptions*/; |
Index: trunk/extensions/CommunityVoice/Modules/Ratings.php |
— | — | @@ -0,0 +1,530 @@ |
| 2 | +<?php |
| 3 | + |
| 4 | +abstract class CommunityVoiceRatings { |
| 5 | + |
| 6 | + /* Private Static Functions */ |
| 7 | + |
| 8 | + private static function getScaleFraction( |
| 9 | + $rating, |
| 10 | + $star |
| 11 | + ) { |
| 12 | + if ( floor( $rating ) > $star ) { |
| 13 | + return 6; |
| 14 | + } else if ( floor( $rating ) < $star ) { |
| 15 | + return 0; |
| 16 | + } else { |
| 17 | + return round( ( 6 / 10 ) * ( ( $rating - floor( $rating ) ) * 10 ) ); |
| 18 | + } |
| 19 | + } |
| 20 | + |
| 21 | + private static function getArticlesUsing( |
| 22 | + $category, |
| 23 | + $title |
| 24 | + ) { |
| 25 | + $dbr = wfGetDB( DB_SLAVE ); |
| 26 | + $result = $dbr->select( |
| 27 | + 'cv_ratings_usage', |
| 28 | + 'usg_article', |
| 29 | + array( |
| 30 | + 'usg_category' => $category, |
| 31 | + 'usg_title' => $title, |
| 32 | + ) |
| 33 | + ); |
| 34 | + $articles = array(); |
| 35 | + while ( $row = $result->fetchRow() ) { |
| 36 | + $articles[] = (string)$row['usg_article']; |
| 37 | + } |
| 38 | + return $articles; |
| 39 | + } |
| 40 | + |
| 41 | + private static function getCategories() { |
| 42 | + $dbr = wfGetDB( DB_SLAVE ); |
| 43 | + $result = $dbr->select( |
| 44 | + 'cv_ratings_votes', |
| 45 | + 'DISTINCT vot_category' |
| 46 | + ); |
| 47 | + $categories = array(); |
| 48 | + while ( $row = $result->fetchRow() ) { |
| 49 | + $categories[] = (string)$row['vot_category']; |
| 50 | + } |
| 51 | + return $categories; |
| 52 | + } |
| 53 | + |
| 54 | + private static function getTitles( |
| 55 | + $category |
| 56 | + ) { |
| 57 | + $dbr = wfGetDB( DB_SLAVE ); |
| 58 | + $result = $dbr->select( |
| 59 | + 'cv_ratings_votes', |
| 60 | + 'DISTINCT vot_title', |
| 61 | + array( 'vot_category' => $category ) |
| 62 | + ); |
| 63 | + $titles = array(); |
| 64 | + while ( $row = $result->fetchRow() ) { |
| 65 | + $titles[] = (string)$row['vot_title']; |
| 66 | + } |
| 67 | + return $titles; |
| 68 | + } |
| 69 | + |
| 70 | + private static function getTotalVotes( |
| 71 | + $category, |
| 72 | + $title |
| 73 | + ) { |
| 74 | + $dbr = wfGetDB( DB_SLAVE ); |
| 75 | + return (integer)$dbr->selectField( |
| 76 | + 'cv_ratings_votes', |
| 77 | + 'COUNT(*)', |
| 78 | + array( |
| 79 | + 'vot_category' => $category, |
| 80 | + 'vot_title' => $title, |
| 81 | + ) |
| 82 | + ); |
| 83 | + } |
| 84 | + |
| 85 | + private static function getUserVoted( |
| 86 | + $category, |
| 87 | + $title |
| 88 | + ) { |
| 89 | + global $wgUser; |
| 90 | + $dbr = wfGetDB( DB_SLAVE ); |
| 91 | + return (bool)$dbr->selectField( |
| 92 | + 'cv_ratings_votes', |
| 93 | + 'COUNT(*)', |
| 94 | + array( |
| 95 | + 'vot_category' => $category, |
| 96 | + 'vot_title' => $title, |
| 97 | + 'vot_user' => $wgUser->getId(), |
| 98 | + ) |
| 99 | + ); |
| 100 | + } |
| 101 | + |
| 102 | + private static function getAverageRating( |
| 103 | + $category, |
| 104 | + $title |
| 105 | + ) { |
| 106 | + $dbr = wfGetDB( DB_SLAVE ); |
| 107 | + return (float)$dbr->selectField( |
| 108 | + 'cv_ratings_votes', |
| 109 | + 'AVG(vot_rating)', |
| 110 | + array( |
| 111 | + 'vot_category' => $category, |
| 112 | + 'vot_title' => $title, |
| 113 | + ) |
| 114 | + ); |
| 115 | + } |
| 116 | + |
| 117 | + private static function addVote( |
| 118 | + $category, |
| 119 | + $title, |
| 120 | + $rating |
| 121 | + ) { |
| 122 | + global $wgUser; |
| 123 | + // Checks if... |
| 124 | + if ( |
| 125 | + // User is logged in |
| 126 | + $wgUser->isLoggedIn() && |
| 127 | + // User has not yet voted |
| 128 | + !self::getUserVoted( $category, $title ) |
| 129 | + ) { |
| 130 | + // Get database connection |
| 131 | + $dbw = wfGetDB( DB_MASTER ); |
| 132 | + // Insert new vote for user |
| 133 | + $dbw->insert( |
| 134 | + 'cv_ratings_votes', |
| 135 | + array( |
| 136 | + 'vot_category' => $category, |
| 137 | + 'vot_title' => $title, |
| 138 | + 'vot_user' => $wgUser->getId(), |
| 139 | + 'vot_rating' => $rating, |
| 140 | + ) |
| 141 | + ); |
| 142 | + $dbw->commit(); |
| 143 | + return true; |
| 144 | + } |
| 145 | + return false; |
| 146 | + } |
| 147 | + |
| 148 | + /* Static Functions */ |
| 149 | + |
| 150 | + public static function register() { |
| 151 | + global $wgParser, $wgAjaxExportList, $wgHooks; |
| 152 | + // Register the hook with the parser |
| 153 | + $wgParser->setHook( 'ratings:scale', array( __CLASS__, 'renderScale' ) ); |
| 154 | + // Register ajax response hook |
| 155 | + $wgAjaxExportList[] = __CLASS__ . '::handleScaleVoteCall'; |
| 156 | + // Register article save hook |
| 157 | + $wgHooks['ArticleSave'][] = __CLASS__ . '::updateArticleUsage'; |
| 158 | + |
| 159 | + } |
| 160 | + |
| 161 | + public static function updateArticleUsage( |
| 162 | + $article, |
| 163 | + $user, |
| 164 | + $text, |
| 165 | + $summary, |
| 166 | + $minor, |
| 167 | + $watch, |
| 168 | + $sectionanchor, |
| 169 | + $flags |
| 170 | + ) { |
| 171 | + $usedRatings = array(); |
| 172 | + // Extract all ratings:scale tags |
| 173 | + preg_match_all( |
| 174 | + "/<ratings:scale[^>]*[\\/]*>/i", $text, $matches, PREG_PATTERN_ORDER |
| 175 | + ); |
| 176 | + // Loop over each match |
| 177 | + foreach( $matches[0] as $match ) { |
| 178 | + $rating = array(); |
| 179 | + foreach ( array( 'category', 'title' ) as $attribute ) { |
| 180 | + // Extract value of attribute |
| 181 | + preg_match( |
| 182 | + "/{$attribute}=['\"]*(?<value>[^'\"]*)['\"]*/i", |
| 183 | + $match, |
| 184 | + $values |
| 185 | + ); |
| 186 | + if ( isset( $values['value'] ) ) { |
| 187 | + $rating[$attribute] = $values['value']; |
| 188 | + } |
| 189 | + } |
| 190 | + if ( isset( $rating['category'], $rating['title'] ) ) { |
| 191 | + $usedRatings[] = $rating; |
| 192 | + } |
| 193 | + } |
| 194 | + // Gets name of article |
| 195 | + $articleDbKey = $article->getTitle()->getPrefixedDBkey(); |
| 196 | + // Get database connection |
| 197 | + $dbw = wfGetDB( DB_MASTER ); |
| 198 | + // Remove all usage for this article |
| 199 | + $dbw->delete( |
| 200 | + 'cv_ratings_usage', |
| 201 | + array( 'usg_article' => $articleDbKey ) |
| 202 | + ); |
| 203 | + // Loop over each rating |
| 204 | + foreach ( $usedRatings as $rating ) { |
| 205 | + // Add usage for rating for this article |
| 206 | + $dbw->insert( |
| 207 | + 'cv_ratings_usage', |
| 208 | + array( |
| 209 | + 'usg_category' => $rating['category'], |
| 210 | + 'usg_title' => $rating['title'], |
| 211 | + 'usg_article' => $articleDbKey, |
| 212 | + ) |
| 213 | + ); |
| 214 | + } |
| 215 | + return true; |
| 216 | + } |
| 217 | + |
| 218 | + public static function renderScale( |
| 219 | + $input, |
| 220 | + $args, |
| 221 | + $parser |
| 222 | + ) { |
| 223 | + global $wgUser, $wgTitle; |
| 224 | + global $egCommunityVoiceResourcesPath; |
| 225 | + // Validate and sanitize incoming arguments |
| 226 | + $errors = array(); |
| 227 | + $error = false; |
| 228 | + foreach ( array( 'category', 'title' ) as $argument ) { |
| 229 | + if ( isset( $args[$argument] ) ) { |
| 230 | + $args[$argument] = htmlspecialchars( $args[$argument] ); |
| 231 | + } else { |
| 232 | + $error = true; |
| 233 | + if ( $parser->getOptions()->getIsPreview() ) { |
| 234 | + $errors[] = CommunityVoice::getMessage( |
| 235 | + 'ratings', 'error-missing-argument', $argument |
| 236 | + ); |
| 237 | + } |
| 238 | + } |
| 239 | + } |
| 240 | + // Checks if an error ocurred |
| 241 | + if ( $error ) { |
| 242 | + // Checks if there are any error messages to return |
| 243 | + if ( count( $errors ) ) { |
| 244 | + return Html::div( |
| 245 | + array( 'class' => 'error' ), implode( ' ', $errors ) |
| 246 | + ); |
| 247 | + } |
| 248 | + // Continues without rendering |
| 249 | + return true; |
| 250 | + } |
| 251 | + // Collects data |
| 252 | + $totalVotes = self::getTotalVotes( $args['category'], $args['title'] ); |
| 253 | + $rating = self::getAverageRating( $args['category'], $args['title'] ); |
| 254 | + $userVoted = self::getUserVoted( $args['category'], $args['title'] ); |
| 255 | + // Builds sanitized HTML id with prepended module naming |
| 256 | + $id = Html::toId( |
| 257 | + 'cv_ratings_scale_' . $args['category'] . '_' . $args['title'] |
| 258 | + ); |
| 259 | + // Gets stats message |
| 260 | + $stats = CommunityVoice::getMessage( |
| 261 | + 'ratings', 'scale-stats', array( round( $rating, 1 ), $totalVotes ) |
| 262 | + ); |
| 263 | + // Begins rating scale |
| 264 | + $htmlOut = Html::open( |
| 265 | + 'div', |
| 266 | + array( 'class' => 'communityvoice-ratings-scale', 'id' => $id ) |
| 267 | + ); |
| 268 | + // Checks for input |
| 269 | + if ( $input != '' ) { |
| 270 | + // Adds content of tag as parsed wiki-text |
| 271 | + $htmlOut .= $parser->recursiveTagParse( $input ); |
| 272 | + } |
| 273 | + // Checks if the user has not voted yet and is logged in |
| 274 | + if ( !$userVoted && $wgUser->isLoggedIn() ) { |
| 275 | + |
| 276 | + /* Ajax Interaction */ |
| 277 | + |
| 278 | + // Adds scale script |
| 279 | + $htmlOut .= Html::script( |
| 280 | + Js::callFunction( |
| 281 | + 'communityVoice.ratings.scales.add', |
| 282 | + Js::buildInstance( |
| 283 | + 'CommunityVoiceRatingsScale', |
| 284 | + array( |
| 285 | + Js::toScalar( $id ), |
| 286 | + Js::toScalar( $args['category'] ), |
| 287 | + Js::toScalar( $args['title'] ), |
| 288 | + Js::toScalar( $rating ), |
| 289 | + Js::toObject( |
| 290 | + array( |
| 291 | + 'stats' => $stats, |
| 292 | + 'status' => array( |
| 293 | + 'null' => ' ', |
| 294 | + 'sending' => CommunityVoice::getMessage( |
| 295 | + 'ratings', 'scale-status-sending' |
| 296 | + ), |
| 297 | + 'error' => CommunityVoice::getMessage( |
| 298 | + 'ratings', 'scale-status-error' |
| 299 | + ), |
| 300 | + 'thanks' => CommunityVoice::getMessage( |
| 301 | + 'ratings', 'scale-status-thanks' |
| 302 | + ), |
| 303 | + ) |
| 304 | + ) |
| 305 | + ), |
| 306 | + Js::toScalar( $wgTitle->getPrefixedText() ) |
| 307 | + ) |
| 308 | + ) |
| 309 | + ) |
| 310 | + ); |
| 311 | + |
| 312 | + /* HTML Form Interaction */ |
| 313 | + |
| 314 | + // Begins non-javascript fallback |
| 315 | + $htmlOut .= Html::open( 'noscript' ); |
| 316 | + // Begins form |
| 317 | + $specialPageTitle = Title::newFromText( 'Special:CommunityVoice' ); |
| 318 | + $htmlOut .= Html::open( |
| 319 | + 'form', |
| 320 | + array( |
| 321 | + 'action' => $specialPageTitle->getFullUrl(), |
| 322 | + 'method' => 'post', |
| 323 | + ) |
| 324 | + ); |
| 325 | + // Builds list of hidden fields |
| 326 | + $hiddenFields = array( |
| 327 | + 'token' => $wgUser->editToken(), |
| 328 | + 'module' => 'Ratings', |
| 329 | + 'action' => 'ScaleVoteSubmission', |
| 330 | + 'scale[article]' => $wgTitle->getPrefixedText(), |
| 331 | + 'scale[category]' => $args['category'], |
| 332 | + 'scale[title]' => $args['title'], |
| 333 | + ); |
| 334 | + // Loops over each field |
| 335 | + foreach ( $hiddenFields as $name => $value ) { |
| 336 | + // Adds hidden field |
| 337 | + $htmlOut .= Html::input( |
| 338 | + array( |
| 339 | + 'type' => 'hidden', 'name' => $name, 'value' => $value |
| 340 | + ) |
| 341 | + ); |
| 342 | + } |
| 343 | + // Loops 5 times (once per star) |
| 344 | + for ( $i = 0; $i < 5; $i++ ) { |
| 345 | + // Adds star as image input |
| 346 | + $htmlOut .= Html::input( |
| 347 | + array( |
| 348 | + 'type' => 'image', |
| 349 | + 'name' => 'scale[rating_' . $i . ']', |
| 350 | + 'src' => sprintf( |
| 351 | + '%s/Icons/star-%d.png', |
| 352 | + $egCommunityVoiceResourcesPath, |
| 353 | + self::getScaleFraction( $rating, $i ) |
| 354 | + ), |
| 355 | + 'border' => 0, |
| 356 | + 'alt' => '', |
| 357 | + 'class' => 'star', |
| 358 | + 'align' => 'absmiddle', |
| 359 | + ) |
| 360 | + ); |
| 361 | + } |
| 362 | + // Adds stats message |
| 363 | + $htmlOut .= Html::tag( 'span', array( 'class' => 'stats' ), $stats ); |
| 364 | + // Ends form |
| 365 | + $htmlOut .= Html::close( 'form' ); |
| 366 | + // Ends non-javascript fallback |
| 367 | + $htmlOut .= Html::close( 'noscript' ); |
| 368 | + } else { |
| 369 | + |
| 370 | + /* No Interaction */ |
| 371 | + |
| 372 | + // Loops 5 times (once per star) |
| 373 | + for ( $i = 0; $i < 5; $i++ ) { |
| 374 | + // Adds star as image |
| 375 | + $htmlOut .= Html::tag( |
| 376 | + 'img', |
| 377 | + array( |
| 378 | + 'src' => sprintf( |
| 379 | + '%s/Icons/star-%d.png', |
| 380 | + $egCommunityVoiceResourcesPath, |
| 381 | + self::getScaleFraction( $rating, $i ) |
| 382 | + ), |
| 383 | + 'border' => 0, |
| 384 | + 'alt' => '', |
| 385 | + 'class' => 'star', |
| 386 | + 'align' => 'absmiddle', |
| 387 | + ) |
| 388 | + ); |
| 389 | + } |
| 390 | + // Adds stats message |
| 391 | + $htmlOut .= Html::tag( 'span', array( 'class' => 'stats' ), $stats ); |
| 392 | + } |
| 393 | + // Ends scale |
| 394 | + $htmlOut .= Xml::closeElement( 'div' ); |
| 395 | + // Returns output |
| 396 | + return $htmlOut; |
| 397 | + } |
| 398 | + |
| 399 | + /* Processing Functions */ |
| 400 | + |
| 401 | + /** |
| 402 | + * Hanlder for ratings scale vote via ajax call |
| 403 | + */ |
| 404 | + public static function handleScaleVoteCall( |
| 405 | + $category, |
| 406 | + $title, |
| 407 | + $rating, |
| 408 | + $article |
| 409 | + ) { |
| 410 | + global $wgUser; |
| 411 | + // Adds vote and checks for success |
| 412 | + if ( self::addVote( $category, $title, $rating ) ) { |
| 413 | + // Gets new rating data |
| 414 | + $rating = self::getAverageRating( $category, $title ); |
| 415 | + // Builds result |
| 416 | + $result = array( |
| 417 | + 'rating' => $rating, |
| 418 | + 'stats' => CommunityVoice::getMessage( |
| 419 | + 'ratings', |
| 420 | + 'scale-stats', |
| 421 | + array( |
| 422 | + round( $rating, 1 ), |
| 423 | + self::getTotalVotes( $category, $title ) |
| 424 | + ) |
| 425 | + ), |
| 426 | + ); |
| 427 | + // Gets articles that use this rating |
| 428 | + $articles = self::getArticlesUsing( $category, $title ); |
| 429 | + // Loops over each article |
| 430 | + foreach($articles as $article ) { |
| 431 | + // Invalidates the cache of the article |
| 432 | + CommunityVoice::touchArticle( $article ); |
| 433 | + } |
| 434 | + // Ensure database commits take place (since this is an ajax call) |
| 435 | + $dbw = wfGetDB( DB_MASTER ); |
| 436 | + $dbw->commit(); |
| 437 | + // Returns result |
| 438 | + return Js::toObject( $result ); |
| 439 | + } |
| 440 | + // Returns error information |
| 441 | + return Js::toObject( array( 'rating' => -1, 'stats' => null ) ); |
| 442 | + } |
| 443 | + |
| 444 | + /** |
| 445 | + * Hanlder for ratings scale vote via HTML form submission |
| 446 | + */ |
| 447 | + public static function handleScaleVoteSubmission() { |
| 448 | + global $wgOut, $wgRequest; |
| 449 | + // Gets scale data |
| 450 | + $scale = $wgRequest->getArray( 'scale' ); |
| 451 | + // Checks if an article was given |
| 452 | + if ( isset( $scale['article'], $scale['title'], $scale['category'] ) ) { |
| 453 | + // Looks for rating value |
| 454 | + foreach ( $scale as $key => $value ) { |
| 455 | + // Breaks key into parts |
| 456 | + $parts = explode( '_', $key ); |
| 457 | + // Checks if... |
| 458 | + if ( |
| 459 | + // There's at least 2 parts |
| 460 | + ( count( $parts ) > 1 ) && |
| 461 | + // The first part is 'rating' |
| 462 | + ( $parts[0] == 'rating' ) && |
| 463 | + // The second part is a number |
| 464 | + ( is_numeric( $parts[1] ) ) |
| 465 | + ) { |
| 466 | + // Uses number as rating |
| 467 | + $rating = $parts[1]; |
| 468 | + // Finishes loop |
| 469 | + break; |
| 470 | + } |
| 471 | + } |
| 472 | + // Checks if a rating was found |
| 473 | + if ( isset( $rating ) ) { |
| 474 | + // Adds vote and checks for success |
| 475 | + if ( |
| 476 | + self::addVote( |
| 477 | + $scale['category'], $scale['title'], $rating |
| 478 | + ) |
| 479 | + ) { |
| 480 | + // Gets articles that use this rating |
| 481 | + $articles = self::getArticlesUsing( |
| 482 | + $scale['category'], $scale['title'] |
| 483 | + ); |
| 484 | + // Loops over each article |
| 485 | + foreach($articles as $article ) { |
| 486 | + // Invalidates the cache of the article |
| 487 | + CommunityVoice::touchArticle( $article ); |
| 488 | + } |
| 489 | + // Redirects user back to article |
| 490 | + $wgOut->redirect( |
| 491 | + Title::newFromText( $scale['article'] )->getFullUrl() |
| 492 | + ); |
| 493 | + } else { |
| 494 | + throw new MWException( 'Voting failed!' ); |
| 495 | + } |
| 496 | + } else { |
| 497 | + throw new MWException( 'No rating parameter!' ); |
| 498 | + } |
| 499 | + } else { |
| 500 | + throw new MWException( 'Missing parameters!' ); |
| 501 | + } |
| 502 | + } |
| 503 | + |
| 504 | + /* UI Functions */ |
| 505 | + |
| 506 | + /** |
| 507 | + * Outputs a summary UI for the module |
| 508 | + */ |
| 509 | + public static function showSummary() { |
| 510 | + global $wgOut; |
| 511 | + // |
| 512 | + $wgOut->addWikiText( '==== Categories ====' ); |
| 513 | + $xmlCategories = Html::open( 'ul' ); |
| 514 | + foreach( self::getCategories() as $category ) { |
| 515 | + $xmlCategories .= Html::tag( 'li', $category ); |
| 516 | + } |
| 517 | + $xmlCategories .= Html::close( 'ul' ); |
| 518 | + $wgOut->addHtml( $xmlCategories ); |
| 519 | + } |
| 520 | + |
| 521 | + /** |
| 522 | + * Outputs main UI for module |
| 523 | + */ |
| 524 | + public static function showMain( |
| 525 | + $path |
| 526 | + ) { |
| 527 | + global $wgOut; |
| 528 | + // |
| 529 | + $wgOut->addWikiText( '==== Detailed Information ====' ); |
| 530 | + } |
| 531 | +} |
\ No newline at end of file |
Index: trunk/extensions/CommunityVoice/Resources/CommunityVoice.js |
— | — | @@ -0,0 +1,243 @@ |
| 2 | +/* JavaScript Document */ |
| 3 | + |
| 4 | +/* Prototypes */ |
| 5 | + |
| 6 | +/** |
| 7 | + * Generic global access system |
| 8 | + */ |
| 9 | +function CommunityVoicePool() { |
| 10 | + |
| 11 | + /* Private Members */ |
| 12 | + |
| 13 | + var self = this; |
| 14 | + var objects = {}; |
| 15 | + |
| 16 | + /* Public Functions */ |
| 17 | + |
| 18 | + /** |
| 19 | + * Adds an object to pool, and returns it's unique ID |
| 20 | + * @param object Object reference to add |
| 21 | + */ |
| 22 | + this.add = function( |
| 23 | + object |
| 24 | + ) { |
| 25 | + if ( typeof( object.getId ) == 'function' ) { |
| 26 | + objects[object.getId()] = object; |
| 27 | + return true; |
| 28 | + } |
| 29 | + return false; |
| 30 | + } |
| 31 | + /** |
| 32 | + * Removes an object form pool |
| 33 | + * @param id ID number of object to remove |
| 34 | + */ |
| 35 | + this.remove = function( |
| 36 | + id |
| 37 | + ) { |
| 38 | + if ( objects[id] !== undefined ) { |
| 39 | + delete objects[id]; |
| 40 | + return true; |
| 41 | + } |
| 42 | + return false; |
| 43 | + } |
| 44 | + /** |
| 45 | + * Gets an object from pool |
| 46 | + * @param id ID number of object to get |
| 47 | + */ |
| 48 | + this.get = function( |
| 49 | + id |
| 50 | + ) { |
| 51 | + if ( objects[id] !== undefined ) { |
| 52 | + return objects[id]; |
| 53 | + } |
| 54 | + return null; |
| 55 | + } |
| 56 | +} |
| 57 | + |
| 58 | +/** |
| 59 | + * Ratings Scale Object |
| 60 | + */ |
| 61 | +function CommunityVoiceRatingsScale( |
| 62 | + id, |
| 63 | + category, |
| 64 | + title, |
| 65 | + rating, |
| 66 | + messages, |
| 67 | + article |
| 68 | +) { |
| 69 | + /* Members */ |
| 70 | + |
| 71 | + var self = this; |
| 72 | + // Gets object references |
| 73 | + var element = document.getElementById( id ); |
| 74 | + var stars = []; |
| 75 | + var labels = {}; |
| 76 | + // Sets state |
| 77 | + var status = null; |
| 78 | + var locked = true; |
| 79 | + |
| 80 | + /* Functions */ |
| 81 | + |
| 82 | + this.getId = function() { |
| 83 | + return id; |
| 84 | + } |
| 85 | + |
| 86 | + this.rate = function( |
| 87 | + newRating |
| 88 | + ) { |
| 89 | + // Checks if a save is already taking place |
| 90 | + if ( locked ) { |
| 91 | + // Exits function immediately |
| 92 | + return; |
| 93 | + } |
| 94 | + // Sets message to sending |
| 95 | + status = 'sending'; |
| 96 | + // Updates UI |
| 97 | + self.update(); |
| 98 | + // Don't allow any more requests |
| 99 | + self.lock(); |
| 100 | + // Saves current request type |
| 101 | + var oldRequestType = sajax_request_type; |
| 102 | + // Changes request type to post |
| 103 | + sajax_request_type = "POST"; |
| 104 | + // Performs asynchronous save on server |
| 105 | + sajax_do_call( |
| 106 | + "CommunityVoiceRatings::handleScaleVoteCall", |
| 107 | + [ category, title, newRating, article ], |
| 108 | + new Function( |
| 109 | + "request", |
| 110 | + "communityVoice.ratings.scales." + |
| 111 | + "get( '" + id + "' ).respond( request )" |
| 112 | + ) |
| 113 | + ); |
| 114 | + // Restores current request type |
| 115 | + sajax_request_type = oldRequestType; |
| 116 | + } |
| 117 | + |
| 118 | + this.respond = function( |
| 119 | + request |
| 120 | + ) { |
| 121 | + /* |
| 122 | + * If errors are happening, this will help with debugging: |
| 123 | + * alert( request.responseText ); |
| 124 | + */ |
| 125 | + if ( request.responseText.substr( 0, 1 ) !== '{' ) { |
| 126 | + // Something VERY wrong just happened |
| 127 | + // Changes message to error |
| 128 | + status = 'error'; |
| 129 | + // Updates UI |
| 130 | + self.update(); |
| 131 | + return; |
| 132 | + } |
| 133 | + // Parse JSON response |
| 134 | + eval( 'var response = ' + request.responseText ); |
| 135 | + // Checks that an error did not occur |
| 136 | + if ( response.rating !== undefined && response.stats !== undefined ) { |
| 137 | + if ( response.rating >= 0 ) { |
| 138 | + // Changes message to received |
| 139 | + status = 'thanks'; |
| 140 | + // Uses response data |
| 141 | + messages.stats = response.stats; |
| 142 | + rating = response.rating; |
| 143 | + } else { |
| 144 | + // Alerts user of error |
| 145 | + status = 'error'; |
| 146 | + } |
| 147 | + } else { |
| 148 | + // Alerts user of error |
| 149 | + status = 'error'; |
| 150 | + } |
| 151 | + // Updates UI |
| 152 | + self.update(); |
| 153 | + } |
| 154 | + |
| 155 | + this.lock = function() { |
| 156 | + locked = true; |
| 157 | + for ( star in stars ) { |
| 158 | + stars[star].style.cursor = 'default'; |
| 159 | + } |
| 160 | + } |
| 161 | + |
| 162 | + this.unlock = function() { |
| 163 | + locked = false; |
| 164 | + for ( star in stars ) { |
| 165 | + stars[star].style.cursor = 'pointer'; |
| 166 | + } |
| 167 | + } |
| 168 | + |
| 169 | + this.update = function( |
| 170 | + hoveredStar |
| 171 | + ) { |
| 172 | + var dir = egCommunityVoiceResourcesPath + '/Icons'; |
| 173 | + var fraction = 0; |
| 174 | + // Change UI accordingly |
| 175 | + for ( star in stars ) { |
| 176 | + if ( Math.floor( rating ) > star ) { |
| 177 | + fraction = 6; |
| 178 | + } else if ( Math.floor( rating ) < star ) { |
| 179 | + fraction = 0; |
| 180 | + } else { |
| 181 | + fraction = Math.round( |
| 182 | + ( 6 / 10 ) * ( ( rating - Math.floor( rating ) ) * 10 ) |
| 183 | + ); |
| 184 | + } |
| 185 | + if ( hoveredStar !== undefined && hoveredStar >= star && !locked ) { |
| 186 | + stars[star].src = dir + '/star-' + fraction + '-hover.png' |
| 187 | + } else { |
| 188 | + stars[star].src = dir + '/star-' + fraction + '.png' |
| 189 | + } |
| 190 | + } |
| 191 | + labels.stats.innerHTML = messages.stats; |
| 192 | + labels.stats.className = 'stats'; |
| 193 | + labels.status.innerHTML = messages.status[status]; |
| 194 | + labels.status.className = status; |
| 195 | + } |
| 196 | + |
| 197 | + // Loops 5 times (once per star) |
| 198 | + for ( var i = 0; i < 5; i++ ) { |
| 199 | + // Creates a new image |
| 200 | + stars[i] = document.createElement( 'img' ); |
| 201 | + stars[i].style.borderWidth = '0px'; |
| 202 | + stars[i].className = 'star'; |
| 203 | + // Adds handlers to image |
| 204 | + addHandler( |
| 205 | + stars[i], |
| 206 | + 'click', |
| 207 | + new Function( |
| 208 | + "communityVoice.ratings.scales." + |
| 209 | + "get( '" + id + "' ).rate( " + ( i + 1 ) + " )" |
| 210 | + ) |
| 211 | + ); |
| 212 | + addHandler( |
| 213 | + stars[i], |
| 214 | + 'mouseover', |
| 215 | + new Function( |
| 216 | + "communityVoice.ratings.scales." + |
| 217 | + "get( '" + id + "' ).update( " + i + " )" |
| 218 | + ) |
| 219 | + ); |
| 220 | + addHandler( |
| 221 | + stars[i], |
| 222 | + 'mouseout', |
| 223 | + new Function( |
| 224 | + "communityVoice.ratings.scales." + |
| 225 | + "get( '" + id + "' ).update()" |
| 226 | + ) |
| 227 | + ); |
| 228 | + // Inserts image into element |
| 229 | + element.appendChild( stars[i] ); |
| 230 | + } |
| 231 | + // Adds labels |
| 232 | + labels.stats = document.createElement( 'span' ); |
| 233 | + element.appendChild( labels.stats ); |
| 234 | + labels.status = document.createElement( 'span' ); |
| 235 | + element.appendChild( labels.status ); |
| 236 | + this.unlock(); |
| 237 | + this.update(); |
| 238 | +} |
| 239 | + |
| 240 | +/* Globals */ |
| 241 | + |
| 242 | +var communityVoice = {}; |
| 243 | +communityVoice.ratings = {}; |
| 244 | +communityVoice.ratings.scales = new CommunityVoicePool(); |
\ No newline at end of file |
Index: trunk/extensions/CommunityVoice/Resources/Icons/star-0.png |
Cannot display: file marked as a binary type. |
svn:mime-type = application/octet-stream |
Property changes on: trunk/extensions/CommunityVoice/Resources/Icons/star-0.png |
___________________________________________________________________ |
Added: svn:mime-type |
1 | 245 | + application/octet-stream |
Index: trunk/extensions/CommunityVoice/Resources/Icons/star-0-hover.png |
Cannot display: file marked as a binary type. |
svn:mime-type = application/octet-stream |
Property changes on: trunk/extensions/CommunityVoice/Resources/Icons/star-0-hover.png |
___________________________________________________________________ |
Added: svn:mime-type |
2 | 246 | + application/octet-stream |
Index: trunk/extensions/CommunityVoice/Resources/Icons/star-1.png |
Cannot display: file marked as a binary type. |
svn:mime-type = application/octet-stream |
Property changes on: trunk/extensions/CommunityVoice/Resources/Icons/star-1.png |
___________________________________________________________________ |
Added: svn:mime-type |
3 | 247 | + application/octet-stream |
Index: trunk/extensions/CommunityVoice/Resources/Icons/star-1-hover.png |
Cannot display: file marked as a binary type. |
svn:mime-type = application/octet-stream |
Property changes on: trunk/extensions/CommunityVoice/Resources/Icons/star-1-hover.png |
___________________________________________________________________ |
Added: svn:mime-type |
4 | 248 | + application/octet-stream |
Index: trunk/extensions/CommunityVoice/Resources/Icons/star-2.png |
Cannot display: file marked as a binary type. |
svn:mime-type = application/octet-stream |
Property changes on: trunk/extensions/CommunityVoice/Resources/Icons/star-2.png |
___________________________________________________________________ |
Added: svn:mime-type |
5 | 249 | + application/octet-stream |
Index: trunk/extensions/CommunityVoice/Resources/Icons/star-2-hover.png |
Cannot display: file marked as a binary type. |
svn:mime-type = application/octet-stream |
Property changes on: trunk/extensions/CommunityVoice/Resources/Icons/star-2-hover.png |
___________________________________________________________________ |
Added: svn:mime-type |
6 | 250 | + application/octet-stream |
Index: trunk/extensions/CommunityVoice/Resources/Icons/star-3.png |
Cannot display: file marked as a binary type. |
svn:mime-type = application/octet-stream |
Property changes on: trunk/extensions/CommunityVoice/Resources/Icons/star-3.png |
___________________________________________________________________ |
Added: svn:mime-type |
7 | 251 | + application/octet-stream |
Index: trunk/extensions/CommunityVoice/Resources/Icons/star-3-hover.png |
Cannot display: file marked as a binary type. |
svn:mime-type = application/octet-stream |
Property changes on: trunk/extensions/CommunityVoice/Resources/Icons/star-3-hover.png |
___________________________________________________________________ |
Added: svn:mime-type |
8 | 252 | + application/octet-stream |
Index: trunk/extensions/CommunityVoice/Resources/Icons/star-4.png |
Cannot display: file marked as a binary type. |
svn:mime-type = application/octet-stream |
Property changes on: trunk/extensions/CommunityVoice/Resources/Icons/star-4.png |
___________________________________________________________________ |
Added: svn:mime-type |
9 | 253 | + application/octet-stream |
Index: trunk/extensions/CommunityVoice/Resources/Icons/star-4-hover.png |
Cannot display: file marked as a binary type. |
svn:mime-type = application/octet-stream |
Property changes on: trunk/extensions/CommunityVoice/Resources/Icons/star-4-hover.png |
___________________________________________________________________ |
Added: svn:mime-type |
10 | 254 | + application/octet-stream |
Index: trunk/extensions/CommunityVoice/Resources/Icons/star-5.png |
Cannot display: file marked as a binary type. |
svn:mime-type = application/octet-stream |
Property changes on: trunk/extensions/CommunityVoice/Resources/Icons/star-5.png |
___________________________________________________________________ |
Added: svn:mime-type |
11 | 255 | + application/octet-stream |
Index: trunk/extensions/CommunityVoice/Resources/Icons/star-5-hover.png |
Cannot display: file marked as a binary type. |
svn:mime-type = application/octet-stream |
Property changes on: trunk/extensions/CommunityVoice/Resources/Icons/star-5-hover.png |
___________________________________________________________________ |
Added: svn:mime-type |
12 | 256 | + application/octet-stream |
Index: trunk/extensions/CommunityVoice/Resources/Icons/star-6.png |
Cannot display: file marked as a binary type. |
svn:mime-type = application/octet-stream |
Property changes on: trunk/extensions/CommunityVoice/Resources/Icons/star-6.png |
___________________________________________________________________ |
Added: svn:mime-type |
13 | 257 | + application/octet-stream |
Index: trunk/extensions/CommunityVoice/Resources/Icons/star-6-hover.png |
Cannot display: file marked as a binary type. |
svn:mime-type = application/octet-stream |
Property changes on: trunk/extensions/CommunityVoice/Resources/Icons/star-6-hover.png |
___________________________________________________________________ |
Added: svn:mime-type |
14 | 258 | + application/octet-stream |
Index: trunk/extensions/CommunityVoice/Resources/Icons/star.svg |
— | — | @@ -0,0 +1,307 @@ |
| 2 | +<?xml version="1.0" encoding="UTF-8" standalone="no"?> |
| 3 | +<!-- Created with Inkscape (http://www.inkscape.org/) --> |
| 4 | +<svg |
| 5 | + xmlns:dc="http://purl.org/dc/elements/1.1/" |
| 6 | + xmlns:cc="http://creativecommons.org/ns#" |
| 7 | + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" |
| 8 | + xmlns:svg="http://www.w3.org/2000/svg" |
| 9 | + xmlns="http://www.w3.org/2000/svg" |
| 10 | + xmlns:xlink="http://www.w3.org/1999/xlink" |
| 11 | + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" |
| 12 | + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" |
| 13 | + version="1.0" |
| 14 | + width="48" |
| 15 | + height="48" |
| 16 | + id="svg0" |
| 17 | + sodipodi:version="0.32" |
| 18 | + inkscape:version="0.46" |
| 19 | + sodipodi:docname="star.svg" |
| 20 | + inkscape:output_extension="org.inkscape.output.svg.inkscape" |
| 21 | + style="display:inline" |
| 22 | + inkscape:export-filename="/Users/tparscal/Sites/wiki/extensions/CommunityVoice/Resources/Icons/star-0-hover.png" |
| 23 | + inkscape:export-xdpi="47.228661" |
| 24 | + inkscape:export-ydpi="47.228661"> |
| 25 | + <metadata |
| 26 | + id="metadata2465"> |
| 27 | + <rdf:RDF> |
| 28 | + <cc:Work |
| 29 | + rdf:about=""> |
| 30 | + <dc:format>image/svg+xml</dc:format> |
| 31 | + <dc:type |
| 32 | + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> |
| 33 | + </cc:Work> |
| 34 | + </rdf:RDF> |
| 35 | + </metadata> |
| 36 | + <sodipodi:namedview |
| 37 | + inkscape:window-height="786" |
| 38 | + inkscape:window-width="1440" |
| 39 | + inkscape:pageshadow="2" |
| 40 | + inkscape:pageopacity="0.0" |
| 41 | + guidetolerance="10.0" |
| 42 | + gridtolerance="10.0" |
| 43 | + objecttolerance="10.0" |
| 44 | + borderopacity="1.0" |
| 45 | + bordercolor="#666666" |
| 46 | + pagecolor="#ffffff" |
| 47 | + id="base" |
| 48 | + showgrid="false" |
| 49 | + showguides="true" |
| 50 | + inkscape:guide-bbox="true" |
| 51 | + inkscape:zoom="10.375" |
| 52 | + inkscape:cx="21.809994" |
| 53 | + inkscape:cy="24" |
| 54 | + inkscape:window-x="0" |
| 55 | + inkscape:window-y="22" |
| 56 | + inkscape:current-layer="layer9" /> |
| 57 | + <defs |
| 58 | + id="defs"> |
| 59 | + <linearGradient |
| 60 | + id="linearGradient3295"> |
| 61 | + <stop |
| 62 | + offset="0" |
| 63 | + style="stop-color: rgb(252, 249, 251); stop-opacity: 1;" |
| 64 | + id="stop3297" /> |
| 65 | + <stop |
| 66 | + offset="1" |
| 67 | + style="stop-color:rgb(252, 249, 251);stop-opacity:0.46698113" |
| 68 | + id="stop3299" /> |
| 69 | + </linearGradient> |
| 70 | + <linearGradient |
| 71 | + id="linearGradient3287"> |
| 72 | + <stop |
| 73 | + offset="0" |
| 74 | + style="stop-color: rgb(252, 249, 251); stop-opacity: 1;" |
| 75 | + id="stop3289" /> |
| 76 | + <stop |
| 77 | + offset="1" |
| 78 | + style="stop-color: rgb(252, 249, 251); stop-opacity: 0;" |
| 79 | + id="stop3291" /> |
| 80 | + </linearGradient> |
| 81 | + <inkscape:perspective |
| 82 | + sodipodi:type="inkscape:persp3d" |
| 83 | + inkscape:vp_x="0 : 24 : 1" |
| 84 | + inkscape:vp_y="0 : 1000 : 0" |
| 85 | + inkscape:vp_z="48 : 24 : 1" |
| 86 | + inkscape:persp3d-origin="24 : 16 : 1" |
| 87 | + id="perspective2467" /> |
| 88 | + <linearGradient |
| 89 | + id="G0"> |
| 90 | + <stop |
| 91 | + id="s1" |
| 92 | + style="stop-color: rgb(230, 207, 0); stop-opacity: 1;" |
| 93 | + offset="0" /> |
| 94 | + <stop |
| 95 | + id="s2" |
| 96 | + style="stop-color: rgb(253, 233, 74); stop-opacity: 1;" |
| 97 | + offset="1" /> |
| 98 | + </linearGradient> |
| 99 | + <linearGradient |
| 100 | + id="G1"> |
| 101 | + <stop |
| 102 | + id="s3" |
| 103 | + style="stop-color: rgb(252, 249, 251); stop-opacity: 1;" |
| 104 | + offset="0" /> |
| 105 | + <stop |
| 106 | + id="s4" |
| 107 | + style="stop-color: rgb(252, 249, 251); stop-opacity: 0;" |
| 108 | + offset="1" /> |
| 109 | + </linearGradient> |
| 110 | + <linearGradient |
| 111 | + id="G2"> |
| 112 | + <stop |
| 113 | + id="s5" |
| 114 | + style="stop-color: rgb(0, 0, 0); stop-opacity: 0.63;" |
| 115 | + offset="0" /> |
| 116 | + <stop |
| 117 | + id="s6" |
| 118 | + style="stop-color: rgb(0, 0, 0); stop-opacity: 0;" |
| 119 | + offset="1" /> |
| 120 | + </linearGradient> |
| 121 | + <linearGradient |
| 122 | + x1="14.660452" |
| 123 | + y1="7.0243196" |
| 124 | + x2="24.030643" |
| 125 | + y2="34.826122" |
| 126 | + id="G3" |
| 127 | + xlink:href="#G1" |
| 128 | + gradientUnits="userSpaceOnUse" /> |
| 129 | + <radialGradient |
| 130 | + cx="24" |
| 131 | + cy="22" |
| 132 | + r="22" |
| 133 | + fx="24" |
| 134 | + fy="22" |
| 135 | + id="R0" |
| 136 | + xlink:href="#G0" |
| 137 | + gradientUnits="userSpaceOnUse" |
| 138 | + gradientTransform="matrix(1,-0.2,0.2,1,-3.8,6.8)" /> |
| 139 | + <radialGradient |
| 140 | + cx="17.3125" |
| 141 | + cy="25.53125" |
| 142 | + r="9.6875" |
| 143 | + fx="17.3125" |
| 144 | + fy="25.53125" |
| 145 | + id="R1" |
| 146 | + xlink:href="#G2" |
| 147 | + gradientUnits="userSpaceOnUse" |
| 148 | + gradientTransform="matrix(2.4,0,0,0.67,-17.1,24.13494)" /> |
| 149 | + <clipPath |
| 150 | + clipPathUnits="userSpaceOnUse" |
| 151 | + id="clipPath3351"> |
| 152 | + <rect |
| 153 | + style="opacity:0.46698118;fill:#fdfbed;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;display:inline" |
| 154 | + id="rect3353" |
| 155 | + width="11.180724" |
| 156 | + height="47.903614" |
| 157 | + x="36.915661" |
| 158 | + y="0" /> |
| 159 | + </clipPath> |
| 160 | + <clipPath |
| 161 | + clipPathUnits="userSpaceOnUse" |
| 162 | + id="clipPath3358"> |
| 163 | + <rect |
| 164 | + style="opacity:0.46698118;fill:#fdfbed;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" |
| 165 | + id="rect3360" |
| 166 | + width="36.819279" |
| 167 | + height="47.903614" |
| 168 | + x="11.277108" |
| 169 | + y="0" /> |
| 170 | + </clipPath> |
| 171 | + <clipPath |
| 172 | + clipPathUnits="userSpaceOnUse" |
| 173 | + id="clipPath3365"> |
| 174 | + <rect |
| 175 | + y="0" |
| 176 | + x="18.409639" |
| 177 | + height="47.903614" |
| 178 | + width="29.590363" |
| 179 | + id="rect3367" |
| 180 | + style="opacity:0.46698118;fill:#fdfbed;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> |
| 181 | + </clipPath> |
| 182 | + <clipPath |
| 183 | + clipPathUnits="userSpaceOnUse" |
| 184 | + id="clipPath3370"> |
| 185 | + <rect |
| 186 | + style="opacity:0.46698118;fill:#fdfbed;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;display:inline" |
| 187 | + id="rect3372" |
| 188 | + width="23.903616" |
| 189 | + height="47.903614" |
| 190 | + x="24" |
| 191 | + y="0" /> |
| 192 | + </clipPath> |
| 193 | + <clipPath |
| 194 | + clipPathUnits="userSpaceOnUse" |
| 195 | + id="clipPath3377"> |
| 196 | + <rect |
| 197 | + y="0" |
| 198 | + x="30.26506" |
| 199 | + height="47.903614" |
| 200 | + width="17.734943" |
| 201 | + id="rect3379" |
| 202 | + style="opacity:0.46698118;fill:#fdfbed;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;display:inline" /> |
| 203 | + </clipPath> |
| 204 | + </defs> |
| 205 | + <g |
| 206 | + inkscape:groupmode="layer" |
| 207 | + id="layer5" |
| 208 | + inkscape:label="6" |
| 209 | + style="display:inline"> |
| 210 | + <path |
| 211 | + d="M 47,41.23494 C 47,44.82461 36.702552,47.73494 24,47.73494 C 11.297449,47.73494 1,44.82461 1,41.23494 C 1,37.64494 11.297449,33.73494 24,33.73494 C 36.702552,33.73494 47,37.644909 47,41.23494 z" |
| 212 | + id="s" |
| 213 | + style="opacity:0.18446601;fill:url(#R1)" /> |
| 214 | + <path |
| 215 | + d="M 37.310531,41.5 L 24.030644,34.826121 L 10.735101,41.471448 L 13.289408,27.369086 L 2.551164,17.368344 L 17.409702,15.326301 L 24.068641,2.5 L 30.697416,15.340488 L 45.551111,17.414249 L 34.789382,27.392039 L 37.310531,41.5 z" |
| 216 | + id="star" |
| 217 | + style="fill:url(#R0);stroke:#c4a000" /> |
| 218 | + <path |
| 219 | + d="M 35.973752,39.712833 L 24.028333,33.709386 L 12.082531,39.678326 L 14.362768,27.008543 L 4.7776396,18.070249 L 18.056076,16.247921 L 24.062792,4.6783526 L 30.045232,16.255741 L 43.322959,18.11306 L 33.713703,27.034785 L 35.973752,39.712833 z" |
| 220 | + id="ol" |
| 221 | + style="opacity:0.68999999;fill:none;stroke:#ffffff" /> |
| 222 | + </g> |
| 223 | + <g |
| 224 | + inkscape:groupmode="layer" |
| 225 | + id="layer8" |
| 226 | + inkscape:label="5" |
| 227 | + style="display:none"> |
| 228 | + <path |
| 229 | + style="fill:#ffffff;fill-opacity:1;stroke:none;display:inline" |
| 230 | + id="path3341" |
| 231 | + d="M 36.635246,40.704819 L 23.98047,34.345093 L 11.310776,40.677611 L 13.744847,27.239076 L 3.5120747,17.70909 L 17.67119,15.76317 L 24.016679,3.54063 L 30.333424,15.776689 L 44.487925,17.752834 L 34.232772,27.260949 L 36.635246,40.704819 z" |
| 232 | + clip-path="url(#clipPath3351)" /> |
| 233 | + </g> |
| 234 | + <g |
| 235 | + inkscape:groupmode="layer" |
| 236 | + id="layer3" |
| 237 | + inkscape:label="4" |
| 238 | + style="display:none"> |
| 239 | + <path |
| 240 | + style="fill:#ffffff;fill-opacity:1;stroke:none;display:inline" |
| 241 | + id="path3307" |
| 242 | + d="M 36.635246,40.704819 L 23.98047,34.345093 L 11.310776,40.677611 L 13.744847,27.239076 L 3.5120747,17.70909 L 17.67119,15.76317 L 24.016679,3.54063 L 30.333424,15.776689 L 44.487925,17.752834 L 34.232772,27.260949 L 36.635246,40.704819 z" |
| 243 | + clip-path="url(#clipPath3377)" /> |
| 244 | + </g> |
| 245 | + <g |
| 246 | + inkscape:groupmode="layer" |
| 247 | + id="layer2" |
| 248 | + inkscape:label="3" |
| 249 | + style="display:none"> |
| 250 | + <path |
| 251 | + d="M 36.635246,40.704819 L 23.98047,34.345093 L 11.310776,40.677611 L 13.744847,27.239076 L 3.5120747,17.70909 L 17.67119,15.76317 L 24.016679,3.54063 L 30.333424,15.776689 L 44.487925,17.752834 L 34.232772,27.260949 L 36.635246,40.704819 z" |
| 252 | + id="path3304" |
| 253 | + style="fill:#ffffff;fill-opacity:1;stroke:none;display:inline" |
| 254 | + clip-path="url(#clipPath3370)" /> |
| 255 | + </g> |
| 256 | + <g |
| 257 | + inkscape:groupmode="layer" |
| 258 | + id="layer1" |
| 259 | + inkscape:label="2" |
| 260 | + style="display:none"> |
| 261 | + <path |
| 262 | + style="fill:#ffffff;fill-opacity:1;stroke:none;display:inline" |
| 263 | + id="path3301" |
| 264 | + d="M 36.635246,40.704819 L 23.98047,34.345093 L 11.310776,40.677611 L 13.744847,27.239076 L 3.5120747,17.70909 L 17.67119,15.76317 L 24.016679,3.54063 L 30.333424,15.776689 L 44.487925,17.752834 L 34.232772,27.260949 L 36.635246,40.704819 z" |
| 265 | + clip-path="url(#clipPath3365)" /> |
| 266 | + </g> |
| 267 | + <g |
| 268 | + inkscape:groupmode="layer" |
| 269 | + id="layer7" |
| 270 | + inkscape:label="1" |
| 271 | + style="display:none"> |
| 272 | + <path |
| 273 | + clip-path="url(#clipPath3358)" |
| 274 | + d="M 36.635246,40.704819 L 23.98047,34.345093 L 11.310776,40.677611 L 13.744847,27.239076 L 3.5120747,17.70909 L 17.67119,15.76317 L 24.016679,3.54063 L 30.333424,15.776689 L 44.487925,17.752834 L 34.232772,27.260949 L 36.635246,40.704819 z" |
| 275 | + id="path3331" |
| 276 | + style="fill:#ffffff;fill-opacity:1;stroke:none;display:inline" /> |
| 277 | + </g> |
| 278 | + <g |
| 279 | + inkscape:groupmode="layer" |
| 280 | + id="layer4" |
| 281 | + inkscape:label="0" |
| 282 | + style="display:inline"> |
| 283 | + <path |
| 284 | + d="M 36.635246,40.704819 L 23.98047,34.345093 L 11.310776,40.677611 L 13.744847,27.239076 L 3.5120747,17.70909 L 17.67119,15.76317 L 24.016679,3.54063 L 30.333424,15.776689 L 44.487925,17.752834 L 34.232772,27.260949 L 36.635246,40.704819 z" |
| 285 | + id="path2469" |
| 286 | + style="fill:#ffffff;fill-opacity:1;stroke:none" /> |
| 287 | + </g> |
| 288 | + <g |
| 289 | + inkscape:groupmode="layer" |
| 290 | + id="layer6" |
| 291 | + inkscape:label="Shine" |
| 292 | + style="display:inline"> |
| 293 | + <path |
| 294 | + d="M 17.731857,15.79089 L 24.06374,3.58682 L 30.369452,15.794804 L 44.440148,17.761498 L 40.820395,21.121507 C 24.382895,17.434007 31.36502,28.341981 13.251748,30.364721 L 13.819799,27.184324 L 3.660452,17.718688 C 3.660452,17.718688 17.731857,15.79089 17.731857,15.79089 z" |
| 295 | + id="hl" |
| 296 | + style="opacity:0.8;fill:url(#G3);stroke:none;display:inline" /> |
| 297 | + </g> |
| 298 | + <g |
| 299 | + inkscape:groupmode="layer" |
| 300 | + id="layer9" |
| 301 | + inkscape:label="Hover" |
| 302 | + style="display:inline"> |
| 303 | + <path |
| 304 | + style="fill:#ffffff;fill-opacity:1;stroke:#c4a000;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;display:inline" |
| 305 | + id="path2434" |
| 306 | + d="M 31.718345,34.290786 L 23.98807,30.405895 L 16.248682,34.274167 L 17.735554,26.065127 L 11.484781,20.243655 L 20.133994,19.054974 L 24.010188,11.588733 L 27.868824,19.063232 L 36.515219,20.270377 L 30.250774,26.078487 L 31.718345,34.290786 z" /> |
| 307 | + </g> |
| 308 | +</svg> |
Index: trunk/extensions/CommunityVoice/Resources/CommunityVoice.css |
— | — | @@ -0,0 +1,36 @@ |
| 2 | +/* CSS Document */ |
| 3 | + |
| 4 | +div.communityvoice-ratings-scale { |
| 5 | + padding: 2px; |
| 6 | +} |
| 7 | +div.communityvoice-ratings-scale form { |
| 8 | + margin: 0px; |
| 9 | + padding: 0px; |
| 10 | +} |
| 11 | +div.communityvoice-ratings-scale p { |
| 12 | + margin: 0px; |
| 13 | + padding: 0px; |
| 14 | +} |
| 15 | +div.communityvoice-ratings-scale img.star { |
| 16 | + border: 0px; |
| 17 | +} |
| 18 | +div.communityvoice-ratings-scale span.stats { |
| 19 | + padding-left: 10px; |
| 20 | + color: gray; |
| 21 | + font-size: smaller; |
| 22 | +} |
| 23 | +div.communityvoice-ratings-scale span.sending { |
| 24 | + padding-left: 10px; |
| 25 | + color: blue; |
| 26 | + font-size: smaller; |
| 27 | +} |
| 28 | +div.communityvoice-ratings-scale span.error { |
| 29 | + padding-left: 10px; |
| 30 | + color: red; |
| 31 | + font-size: smaller; |
| 32 | +} |
| 33 | +div.communityvoice-ratings-scale span.thanks { |
| 34 | + padding-left: 10px; |
| 35 | + color: green; |
| 36 | + font-size: smaller; |
| 37 | +} |
\ No newline at end of file |
Index: trunk/extensions/CommunityVoice/CommunityVoice.page.php |
— | — | @@ -0,0 +1,87 @@ |
| 2 | +<?php |
| 3 | +/** |
| 4 | + * Special Page Class for CommunityVoice extension |
| 5 | + * |
| 6 | + * @file |
| 7 | + * @ingroup Extensions |
| 8 | + */ |
| 9 | + |
| 10 | +class CommunityVoicePage extends SpecialPage { |
| 11 | + |
| 12 | + /* Functions */ |
| 13 | + |
| 14 | + public function __construct() { |
| 15 | + |
| 16 | + /* Initialization */ |
| 17 | + |
| 18 | + // Initializes special page |
| 19 | + parent::__construct( 'CommunityVoice' ); |
| 20 | + // Loads extension messages |
| 21 | + wfLoadExtensionMessages( 'CommunityVoice' ); |
| 22 | + } |
| 23 | + |
| 24 | + public function execute( |
| 25 | + $sub |
| 26 | + ) { |
| 27 | + global $wgOut, $wgRequest, $wgUser; |
| 28 | + |
| 29 | + /* Control */ |
| 30 | + |
| 31 | + // Gets edit token |
| 32 | + $token = $wgRequest->getText( 'token' ); |
| 33 | + // Checks if a token was given |
| 34 | + if ( $token ) { |
| 35 | + // Validates edit token |
| 36 | + if ( $wgUser->editToken() == $token ) { |
| 37 | + // Gets module |
| 38 | + $module = $wgRequest->getText( 'module' ); |
| 39 | + // Gets action |
| 40 | + $action = $wgRequest->getText( 'action' ); |
| 41 | + // Checks that module and action were given |
| 42 | + if ( $module and $action ) { |
| 43 | + // Calls action on module |
| 44 | + CommunityVoice::callModuleAction( |
| 45 | + $module, 'handle', $action |
| 46 | + ); |
| 47 | + // Finishes page |
| 48 | + return true; |
| 49 | + } else { |
| 50 | + throw new MWException( |
| 51 | + $module . ' module or ' . $action . ' action ' . |
| 52 | + ' not found or are not callable!' |
| 53 | + ); |
| 54 | + } |
| 55 | + } else { |
| 56 | + throw new MWException( 'Invalid edit token!' ); |
| 57 | + } |
| 58 | + } |
| 59 | + |
| 60 | + /* View */ |
| 61 | + |
| 62 | + // Begins output |
| 63 | + $this->setHeaders(); |
| 64 | + // Breaks sub into path steps |
| 65 | + $path = explode( '/', $sub ); |
| 66 | + // Checks if a specific module was given |
| 67 | + if ( count( $path ) > 0 ) { |
| 68 | + // Calls specific action on module |
| 69 | + CommunityVoice::callModuleAction( |
| 70 | + $path[0], 'show', count( $path ) >= 2 ? $path[1] : 'Main' |
| 71 | + ); |
| 72 | + // Finishes page |
| 73 | + return true; |
| 74 | + } |
| 75 | + // Modules summary view |
| 76 | + foreach( CommunityVoice::getModules() as $module ) { |
| 77 | + // Adds heading |
| 78 | + $wgOut->addWikiText( |
| 79 | + '== ' . wfMsg( 'communityvoice-' . $module ) . ' ==' |
| 80 | + ); |
| 81 | + // Calls summary action on module |
| 82 | + self::callModuleAction( $module, 'show', 'Summary' ); |
| 83 | + } |
| 84 | + // Finishes page |
| 85 | + return true; |
| 86 | + } |
| 87 | + |
| 88 | +} |
\ No newline at end of file |
Index: trunk/extensions/CommunityVoice/CommunityVoice.i18n.php |
— | — | @@ -0,0 +1,27 @@ |
| 2 | +<?php |
| 3 | +/** |
| 4 | + * Internationalization for CommunityVoice extension |
| 5 | + * |
| 6 | + * @file |
| 7 | + * @ingroup Extensions |
| 8 | + */ |
| 9 | + |
| 10 | +$messages = array(); |
| 11 | + |
| 12 | +/** English |
| 13 | + * @author Trevor Parscal |
| 14 | + */ |
| 15 | +$messages['en'] = array( |
| 16 | + // Page |
| 17 | + 'communityvoice' => 'Community Voice', |
| 18 | + // Description |
| 19 | + 'communityvoice-desc' => 'Community participation tools', |
| 20 | + // Ratings Module |
| 21 | + 'communityvoice-ratings' => 'Ratings', |
| 22 | + 'communityvoice-ratings-scale-status-sending' => 'Sending...', |
| 23 | + 'communityvoice-ratings-scale-status-error' => 'Error sending!', |
| 24 | + 'communityvoice-ratings-scale-status-thanks' => 'Thanks for voting!', |
| 25 | + 'communityvoice-ratings-scale-stats' =>'$1 / 5 ($2 {{PLURAL:$2|vote|votes}} cast)', |
| 26 | + 'communityvoice-ratings-error-no-category' => 'Category attribute missing in rating tag.', |
| 27 | + 'communityvoice-ratings-error-no-title' => 'Title attribute missing in rating tag.', |
| 28 | +); |
\ No newline at end of file |
Index: trunk/extensions/CommunityVoice/CommunityVoice.php |
— | — | @@ -0,0 +1,189 @@ |
| 2 | +<?php |
| 3 | +/** |
| 4 | + * CommunityVoice extension |
| 5 | + * |
| 6 | + * @file |
| 7 | + * @ingroup Extensions |
| 8 | + * |
| 9 | + * This is the main include file for the CommunityVoice extension. |
| 10 | + * |
| 11 | + * Installation: Add the following line in LocalSettings.php: |
| 12 | + * require_once( "$IP/extensions/CommunityVoice/CommunityVoice.php" ); |
| 13 | + * |
| 14 | + * This extension depends on the ClientSide extension, which provides functions |
| 15 | + * for generating code in client-side formats such as HTML, CSS and JavaScript |
| 16 | + * |
| 17 | + * @author Trevor Parscal <tparscal@wikimedia.org> |
| 18 | + * @license GPL v2 |
| 19 | + * @version 0.1.0 |
| 20 | + */ |
| 21 | + |
| 22 | +// Check environment |
| 23 | +if ( !defined( 'MEDIAWIKI' ) ) { |
| 24 | + echo ( "This is a MediaWiki extension and cannot be run standalone.\n" ); |
| 25 | + die ( 1 ); |
| 26 | +} |
| 27 | + |
| 28 | +/* Configuration */ |
| 29 | + |
| 30 | +// Web-accessable resource path |
| 31 | +$egCommunityVoiceResourcesPath = $wgScriptPath . |
| 32 | + '/extensions/CommunityVoice/Resources'; |
| 33 | + |
| 34 | +/* MediaWiki Integration */ |
| 35 | + |
| 36 | +// Credits |
| 37 | +$wgExtensionCredits['other'][] = array( |
| 38 | + 'name' => 'CommunityVoice', |
| 39 | + 'author' => 'Trevor Parscal', |
| 40 | + 'url' => 'http://www.mediawiki.org/wiki/Extension:CommunityVoice', |
| 41 | + 'description-msg' => 'communityvoice-desc', |
| 42 | +); |
| 43 | +// Shortcut to this extension directory |
| 44 | +$dir = dirname( __FILE__ ) . '/'; |
| 45 | +// Internationalization |
| 46 | +$wgExtensionMessagesFiles['CommunityVoice'] = $dir . 'CommunityVoice.i18n.php'; |
| 47 | +// Class Autoloading |
| 48 | +$wgAutoloadClasses['CommunityVoice'] = $dir . 'CommunityVoice.php'; |
| 49 | +$wgAutoloadClasses['CommunityVoicePage'] = $dir . 'CommunityVoice.page.php'; |
| 50 | +$wgAutoloadClasses['CommunityVoiceRatings'] = $dir . 'Modules/Ratings.php'; |
| 51 | +// Spacial Pages |
| 52 | +$wgSpecialPages['CommunityVoice'] = 'CommunityVoicePage'; |
| 53 | +// Setup Hooks |
| 54 | +$wgExtensionFunctions[] = 'CommunityVoice::registerModules'; |
| 55 | +$wgHooks['AjaxAddScript'][] = 'CommunityVoice::addScripts'; |
| 56 | +$wgHooks['BeforePageDisplay'][] = 'CommunityVoice::addStyles'; |
| 57 | + |
| 58 | +/* Classes */ |
| 59 | + |
| 60 | +abstract class CommunityVoice { |
| 61 | + |
| 62 | + /* Static Members */ |
| 63 | + |
| 64 | + static private $modules = array( |
| 65 | + 'Ratings' => array( 'class' => 'CommunityVoiceRatings' ) |
| 66 | + ); |
| 67 | + static private $messagesLoaded = false; |
| 68 | + |
| 69 | + /* Static Functions */ |
| 70 | + |
| 71 | + public static function getModules() { |
| 72 | + return array_keys( self::$modules ); |
| 73 | + } |
| 74 | + |
| 75 | + public static function callModuleAction( |
| 76 | + $module, |
| 77 | + $type, |
| 78 | + $action = '' |
| 79 | + ) { |
| 80 | + // Checks for class |
| 81 | + if ( isset( self::$modules[$module] ) ) { |
| 82 | + if ( class_exists( self::$modules[$module]['class'] ) ) { |
| 83 | + // Builds function |
| 84 | + $function = array( |
| 85 | + self::$modules[$module]['class'], $type . $action |
| 86 | + ); |
| 87 | + // Checks callability |
| 88 | + if ( is_callable( $function ) ) { |
| 89 | + // Calls function on class |
| 90 | + return call_user_func( $function ); |
| 91 | + } else { |
| 92 | + // Throws unfound/uncallable function exception |
| 93 | + throw new MWException( |
| 94 | + implode( '::', $function ) . |
| 95 | + ' was not found or is not callable!' |
| 96 | + ); |
| 97 | + } |
| 98 | + } else { |
| 99 | + // Throws non-existant class exception |
| 100 | + throw new MWException( |
| 101 | + self::$modules[$module]['class'] . ' is not a class!' |
| 102 | + ); |
| 103 | + } |
| 104 | + } else { |
| 105 | + // Throws non-existant module exception |
| 106 | + throw new MWException( $module . ' is not a module!' ); |
| 107 | + } |
| 108 | + } |
| 109 | + |
| 110 | + /** |
| 111 | + * Registers modules with MediaWiki |
| 112 | + */ |
| 113 | + public static function registerModules() { |
| 114 | + // Loops over each module |
| 115 | + foreach( self::getModules() as $module ) { |
| 116 | + self::callModuleAction( $module, 'register' ); |
| 117 | + } |
| 118 | + return true; |
| 119 | + } |
| 120 | + |
| 121 | + public static function getMessage( |
| 122 | + $module, |
| 123 | + $message, |
| 124 | + $parameter = null |
| 125 | + ) { |
| 126 | + // Checks if extension messages have been loaded already |
| 127 | + if ( !self::$messagesLoaded ) { |
| 128 | + // Loads extension messages |
| 129 | + wfLoadExtensionMessages( 'CommunityVoice' ); |
| 130 | + self::$messagesLoaded = true; |
| 131 | + } |
| 132 | + // Returns message |
| 133 | + return wfMsg( 'communityvoice-' . $module . '-' . $message, $parameter ); |
| 134 | + } |
| 135 | + |
| 136 | + public static function touchArticle( |
| 137 | + $article |
| 138 | + ) { |
| 139 | + // Gets the title of the article which included the scale |
| 140 | + $articleTitle = Title::newFromText( $article ); |
| 141 | + // Invalidates the cache of the article |
| 142 | + $articleTitle->invalidateCache(); |
| 143 | + } |
| 144 | + |
| 145 | + /** |
| 146 | + * Adds scripts to document |
| 147 | + */ |
| 148 | + public static function addScripts( |
| 149 | + $out |
| 150 | + ) { |
| 151 | + global $wgJsMimeType; |
| 152 | + global $egCommunityVoiceResourcesPath; |
| 153 | + $out->addInlineScript( |
| 154 | + sprintf( |
| 155 | + "var egCommunityVoiceResourcesPath = '%s';\n" , |
| 156 | + Xml::escapeJsString( $egCommunityVoiceResourcesPath ) |
| 157 | + ) |
| 158 | + ); |
| 159 | + $out->addScript( |
| 160 | + Xml::element( |
| 161 | + 'script', |
| 162 | + array( |
| 163 | + 'type' => $wgJsMimeType, |
| 164 | + 'src' => $egCommunityVoiceResourcesPath . |
| 165 | + '/CommunityVoice.js' |
| 166 | + ), |
| 167 | + '', |
| 168 | + false |
| 169 | + ) |
| 170 | + ); |
| 171 | + return true; |
| 172 | + } |
| 173 | + |
| 174 | + /** |
| 175 | + * Adds styles to document |
| 176 | + */ |
| 177 | + public static function addStyles( |
| 178 | + $out |
| 179 | + ) { |
| 180 | + global $egCommunityVoiceResourcesPath; |
| 181 | + $out->addLink( |
| 182 | + array( |
| 183 | + 'rel' => 'stylesheet', |
| 184 | + 'type' => 'text/css', |
| 185 | + 'href' => $egCommunityVoiceResourcesPath . '/CommunityVoice.css' |
| 186 | + ) |
| 187 | + ); |
| 188 | + return true; |
| 189 | + } |
| 190 | +} |
\ No newline at end of file |
Property changes on: trunk/extensions/CommunityVoice |
___________________________________________________________________ |
Added: svn:ignore |
1 | 191 | + .project |