Jack Phoenix has submitted this change and it was merged.

Change subject: SVN exported r2730 from ShoutWiki SVN, /branches/jack/Challenge
......................................................................


SVN exported r2730 from ShoutWiki SVN, /branches/jack/Challenge

This extension is still very much under development and not yet suitable
for a production wiki. See the extension's MediaWiki.org info page for
more details.

Change-Id: I87ae4147794558be54c570fd0b7f40c5faf1ae03
---
A Challenge.alias.php
A Challenge.class.php
A Challenge.php
A ChallengeAction.php
A ChallengeHistory.php
A ChallengeStandings.php
A ChallengeUser.php
A ChallengeView.php
A challenge.sql
A i18n/en.json
A i18n/fi.json
A resources/css/ext.challenge.history.css
A resources/css/ext.challenge.standings.css
A resources/css/ext.challenge.user.css
A resources/css/ext.challenge.view.css
A resources/images/userpageIcon.png
A resources/js/Challenge.js
A resources/js/DatePicker.js
A resources/js/ValidateDate.js
A templates/challengeuser.tmpl.php
A templates/challengeview.tmpl.php
21 files changed, 2,538 insertions(+), 0 deletions(-)

Approvals:
  Jack Phoenix: Verified; Looks good to me, approved



diff --git a/Challenge.alias.php b/Challenge.alias.php
new file mode 100644
index 0000000..a93424a
--- /dev/null
+++ b/Challenge.alias.php
@@ -0,0 +1,27 @@
+<?php
+/**
+ * Special page aliases for the Challenge extension.
+ *
+ * @file
+ * @ingroup Extensions
+ */
+
+$aliases = array();
+
+/** English */
+$aliases['en'] = array(
+       'ChallengeAction' => array( 'ChallengeAction' ),
+       'ChallengeHistory' => array( 'ChallengeHistory' ),
+       'ChallengeStandings' => array( 'ChallengeStandings' ),
+       'ChallengeUser' => array( 'ChallengeUser' ),
+       'ChallengeView' => array( 'ChallengeView' ),
+);
+
+/** Finnish (Suomi) */
+$aliases['fi'] = array(
+       'ChallengeAction' => array( 'Haastetoiminto' ),
+       'ChallengeHistory' => array( 'Haastehistoria' ),
+       'ChallengeStandings' => array( 'Haastetilastot' ),
+       'ChallengeUser' => array( 'Haasta_käyttäjä' ),
+       'ChallengeView' => array( 'Tarkastele_haastetta' ),
+);
\ No newline at end of file
diff --git a/Challenge.class.php b/Challenge.class.php
new file mode 100644
index 0000000..e580f86
--- /dev/null
+++ b/Challenge.class.php
@@ -0,0 +1,576 @@
+<?php
+/**
+ * @file
+ */
+class Challenge {
+
+       public $rating_names = array(
+               1 => 'positive',
+               -1 => 'negative',
+               0 => 'neutral'
+       );
+
+       /**
+        * Quickie wrapper function for sending out an email as properly 
rendered
+        * HTML instead of plaintext.
+        *
+        * The functions in this class that call this function used to use
+        * User::sendMail(), but it was causing the mentioned bug, hence why 
this
+        * function had to be introduced.
+        *
+        * @see https://bugzilla.wikimedia.org/show_bug.cgi?id=68045
+        * @see https://gerrit.wikimedia.org/r/#/c/146514/
+        *
+        * @param User $string User (object) whom to send an email
+        * @param string $subject Email subject
+        * @param $string $body Email contents (HTML)
+        * @return Status object
+        */
+       public function sendMail( $user, $subject, $body ) {
+               global $wgPasswordSender;
+               $sender = new MailAddress( $wgPasswordSender,
+                       wfMessage( 'emailsender' )->inContentLanguage()->text() 
);
+               $to = new MailAddress( $user );
+               return UserMailer::send( $to, $sender, $subject, $body, null, 
'text/html; charset=UTF-8' );
+       }
+
+       /**
+        * Add a challenge to the database and send a challenge request mail to 
the
+        * challenged user.
+        *
+        * @param string $user_to Name of the person who was challenged
+        * @param $info
+        * @param $event_date
+        * @param string $description User-supplied description of the challenge
+        * @param string $win_terms User-supplied win terms
+        * @param string $lose_terms User-supplied lose terms
+        */
+       public function addChallenge( $user_to, $info, $event_date, 
$description, $win_terms, $lose_terms ) {
+               global $wgUser;
+
+               $user_id_to = User::idFromName( $user_to );
+
+               $dbw = wfGetDB( DB_MASTER );
+               $dbw->insert(
+                       'challenge',
+                       array(
+                               'challenge_user_id_1' => $wgUser->getId(),
+                               'challenge_username1' => $wgUser->getName(),
+                               'challenge_user_id_2' => $user_id_to,
+                               'challenge_username2' => $user_to,
+                               'challenge_info' => $info,
+                               'challenge_description' => $description,
+                               'challenge_win_terms' => $win_terms,
+                               'challenge_lose_terms' => $lose_terms,
+                               'challenge_status' => 0,
+                               'challenge_date' => $dbw->timestamp(),
+                               'challenge_event_date' => $event_date
+                       ),
+                       __METHOD__
+               );
+
+               $this->challenge_id = $dbw->insertId();
+               $this->sendChallengeRequestEmail( $user_id_to, 
$wgUser->getName(), $this->challenge_id );
+       }
+
+       public function sendChallengeRequestEmail( $user_id_to, $user_from, $id 
) {
+               $user = User::newFromId( $user_id_to );
+               $user->loadFromDatabase();
+
+               if ( $user->isEmailConfirmed() && $user->getIntOption( 
'notifychallenge', 1 ) ) {
+                       $challenge_view_title = SpecialPage::getTitleFor( 
'ChallengeView' );
+                       $update_profile_link = SpecialPage::getTitleFor( 
'UpdateProfile' );
+                       $subject = wfMessage( 'challenge_request_subject', 
$user_from )->text();
+                       $body = wfMessage(
+                               'challenge_request_body',
+                               $user->getName(),
+                               $user_from,
+                               $challenge_view_title->getFullURL( array( 'id' 
=> $id ) ),
+                               $update_profile_link->getFullURL()
+                       )->text();
+                       $this->sendMail( $user, $subject, $body );
+               }
+       }
+
+       public function sendChallengeAcceptEmail( $user_id_to, $user_from, $id 
) {
+               $user = User::newFromId( $user_id_to );
+               $user->loadFromDatabase();
+
+               if ( $user->isEmailConfirmed() && $user->getIntOption( 
'notifychallenge', 1 ) ) {
+                       $challenge_view_title = SpecialPage::getTitleFor( 
'ChallengeView' );
+                       $update_profile_link = SpecialPage::getTitleFor( 
'UpdateProfile' );
+                       $subject = wfMessage( 'challenge_accept_subject', 
$user_from )->text();
+                       $body = wfMessage(
+                               'challenge_accept_body',
+                               $user->getName(),
+                               $user_from,
+                               $challenge_view_title->getFullURL( array( 'id' 
=> $id ) ),
+                               $update_profile_link->getFullURL()
+                       )->text();
+                       $this->sendMail( $user, $subject, $body );
+               }
+       }
+
+       public function sendChallengeLoseEmail( $user_id_to, $user_from, $id ) {
+               $user = User::newFromId( $user_id_to );
+               $user->loadFromDatabase();
+
+               if ( $user->isEmailConfirmed() && $user->getIntOption( 
'notifychallenge', 1 ) ) {
+                       $challenge_view_title = SpecialPage::getTitleFor( 
'ChallengeView' );
+                       $update_profile_link = SpecialPage::getTitleFor( 
'UpdateProfile' );
+                       $subject = wfMessage(
+                               'challenge_lose_subject',
+                               $user_from,
+                               $id
+                       )->parse();
+                       $body = wfMessage(
+                               'challenge_lose_body',
+                               $user->getName(),
+                               $user_from,
+                               $challenge_view_title->getFullURL( array( 'id' 
=> $id ) ),
+                               $update_profile_link->getFullURL()
+                       )->text();
+                       $this->sendMail( $user, $subject, $body );
+               }
+       }
+
+       public function sendChallengeWinEmail( $user_id_to, $user_from, $id ) {
+               $user = User::newFromId( $user_id_to );
+               $user->loadFromDatabase();
+               if ( $user->isEmailConfirmed() && $user->getIntOption( 
'notifychallenge', 1 ) ) {
+                       $challenge_view_title = SpecialPage::getTitleFor( 
'ChallengeView' );
+                       $update_profile_link = SpecialPage::getTitleFor( 
'UpdateProfile' );
+                       $subject = wfMessage( 'challenge_win_subject', 
$user_from, $id )->parse();
+                       $body = wfMessage(
+                               'challenge_win_body',
+                               $user->getName(),
+                               $user_from,
+                               $challenge_view_title->getFullURL( array( 'id' 
=> $id ) ),
+                               $update_profile_link->getFullURL()
+                       )->text();
+                       $this->sendMail( $user, $subject, $body );
+               }
+       }
+
+       /**
+        * Update the status of the given challenge (via its ID) to $status.
+        *
+        * @param int $challenge_id Challenge identifier
+        * @param int $status Status code
+        * @param bool $email Send emails to challenge participants (if they 
have confirmed their addresses)?
+        */
+       public function updateChallengeStatus( $challenge_id, $status, $email = 
true ) {
+               $dbw = wfGetDB( DB_MASTER );
+               $dbw->update(
+                       'challenge',
+                       array( 'challenge_status' => $status ),
+                       array( 'challenge_id' => $challenge_id ),
+                       __METHOD__
+               );
+               $c = $this->getChallenge( $challenge_id );
+
+               switch ( $status ) {
+                       case 1: // challenge was accepted
+                               // Update social stats for both users involved 
in challenge
+                               $stats = new UserStatsTrack( 1, 
$c['user_id_1'], $c['user_name_1'] );
+                               $stats->incStatField( 'challenges' );
+
+                               $stats = new UserStatsTrack( 1, 
$c['user_id_2'], $c['user_name_2'] );
+                               $stats->incStatField( 'challenges' );
+
+                               if ( $email ) {
+                                       $this->sendChallengeAcceptEmail( 
$c['user_id_1'], $c['user_name_2'], $challenge_id );
+                               }
+
+                               break;
+                       case 3: // challenge was completed, send email to loser
+                               $stats = new UserStatsTrack( 1, 
$c['winner_user_id'], $c['winner_user_name'] );
+                               $stats->incStatField( 'challenges_won' );
+
+                               $this->updateUserStandings( $challenge_id );
+                               if ( $c['winner_user_id'] == $c['user_id_1'] ) {
+                                       $loser_id = $c['user_id_2'];
+                                       $loser_name = $c['user_name_2'];
+                               } else {
+                                       $loser_id = $c['user_id_1'];
+                                       $loser_name = $c['user_name_1'];
+                               }
+
+                               if ( $email ) {
+                                       $this->sendChallengeLoseEmail( 
$loser_id, $c['winner_user_name'], $challenge_id );
+                                       $this->sendChallengeWinEmail( 
$c['winner_user_id'], $loser_name, $challenge_id );
+                               }
+                       break;
+               }
+       }
+
+       /**
+        * Update challenge standings for both participants.
+        *
+        * @param int $id Challenge identifier
+        */
+       public function updateUserStandings( $id ) {
+               $dbr = wfGetDB( DB_MASTER );
+               $s = $dbr->selectRow(
+                       'challenge',
+                       array(
+                               'challenge_user_id_1', 'challenge_username1', 
'challenge_user_id_2',
+                               'challenge_username2', 'challenge_info', 
'challenge_event_date',
+                               'challenge_description', 'challenge_win_terms',
+                               'challenge_lose_terms', 
'challenge_winner_user_id',
+                               'challenge_winner_username', 'challenge_status'
+                       ),
+                       array( 'challenge_id' => $id ),
+                       __METHOD__
+               );
+
+               if ( $s !== false ) {
+                       if ( $s->challenge_winner_user_id != -1 ) { // if it's 
not a tie
+                               if ( $s->challenge_user_id_1 == 
$s->challenge_winner_user_id ) {
+                                       $winner_id = $s->challenge_user_id_1;
+                                       $loser_id = $s->challenge_user_id_2;
+                               } else {
+                                       $winner_id = $s->challenge_user_id_2;
+                                       $loser_id = $s->challenge_user_id_1;
+                               }
+                               $this->updateUserRecord( $winner_id, 1 );
+                               $this->updateUserRecord( $loser_id, -1 );
+                       } else {
+                               $this->updateUserRecord( 
$s->challenge_user_id_1, 0 );
+                               $this->updateUserRecord( 
$s->challenge_user_id_2, 0 );
+                       }
+               }
+       }
+
+       public function updateChallengeWinner( $id, $user_id ) {
+               $user = User::newFromId( $user_id );
+               $user_name = $user->getName();
+               $dbr = wfGetDB( DB_MASTER );
+               $dbr->update(
+                       'challenge',
+                       array(
+                               'challenge_winner_user_id' => $user_id,
+                               'challenge_winner_username' => $user_name
+                       ),
+                       array( 'challenge_id' => $id ),
+                       __METHOD__
+               );
+       }
+
+       public function updateUserRecord( $id, $type ) {
+               $user = User::newFromId( $id );
+               $username = $user->getName();
+
+               $dbr = wfGetDB( DB_SLAVE );
+               $dbw = wfGetDB( DB_MASTER );
+               $wins = 0;
+               $losses = 0;
+               $ties = 0;
+
+               $res = $dbr->select(
+                       'challenge_user_record',
+                       array( 'challenge_wins', 'challenge_losses', 
'challenge_ties' ),
+                       array( 'challenge_record_user_id' => $id ),
+                       __METHOD__,
+                       array( 'LIMIT' => 1 )
+               );
+               $row = $dbr->fetchObject( $res );
+               if ( !$row ) {
+                       switch ( $type ) {
+                               case -1:
+                                       $losses = 1;
+                                       break;
+                               case 0:
+                                       $ties = 1;
+                                       break;
+                               case 1:
+                                       $wins = 1;
+                                       break;
+                       }
+                       $dbw->insert(
+                               'challenge_user_record',
+                               array(
+                                       'challenge_record_user_id' => $id,
+                                       'challenge_record_username' => 
$username,
+                                       'challenge_wins' => $wins,
+                                       'challenge_losses' => $losses,
+                                       'challenge_ties' => $ties
+                               ),
+                               __METHOD__
+                       );
+               } else {
+                       $wins = $row->challenge_wins;
+                       $losses = $row->challenge_losses;
+                       $ties = $row->challenge_ties;
+                       switch ( $type ) {
+                               case -1:
+                                       $losses++;
+                                       break;
+                               case 0:
+                                       $ties++;
+                                       break;
+                               case 1:
+                                       $wins++;
+                                       break;
+                       }
+                       $dbw->update(
+                               'challenge_user_record',
+                               array(
+                                       'challenge_wins' => $wins,
+                                       'challenge_losses' => $losses,
+                                       'challenge_ties' => $ties
+                               ),
+                               array( 'challenge_record_user_id' => $id ),
+                               __METHOD__
+                       );
+               }
+       }
+
+       /**
+        * Is the supplied user (ID) a participant in the challenge, identified 
by
+        * its ID?
+        *
+        * @param int $userId User ID
+        * @param int $challengeId Challenge ID
+        * @return bool
+        */
+       public function isUserInChallenge( $userId, $challengeId ) {
+               $dbr = wfGetDB( DB_MASTER );
+               $s = $dbr->selectRow(
+                       'challenge',
+                       array( 'challlenge_user_id_1', 'challlenge_user_id_2' ),
+                       array( 'challenge_id' => $challengeId ),
+                       __METHOD__
+               );
+               if ( $s !== false ) {
+                       if ( $userId == $s->challlenge_user_id_1 || $userId == 
$s->challlenge_user_id_2 ) {
+                               return true;
+                       }
+               }
+               return false;
+       }
+
+       /**
+        * Get the amount of open challenges for the given user (ID).
+        *
+        * @param int $userId User ID
+        * @return int Challenge count for the given user (ID)
+        */
+       static function getOpenChallengeCount( $userId ) {
+               $dbr = wfGetDB( DB_MASTER );
+               $openChallengeCount = 0;
+               $s = $dbr->selectRow(
+                       'challenge',
+                       array( 'COUNT(*) AS count' ),
+                       array( 'challenge_user_id_2' => $userId, 
'challenge_status' => 0 ),
+                       __METHOD__
+               );
+               if ( $s !== false ) {
+                       $openChallengeCount = $s->count;
+               }
+               return $openChallengeCount;
+       }
+
+       /**
+        * Get the amount of total challenges for the given user (ID).
+        *
+        * @param int $userId User ID
+        * @return int Challenge count for the given user (ID)
+        */
+       static function getChallengeCount( $userId = 0 ) {
+               $dbr = wfGetDB( DB_SLAVE );
+               $challengeCount = 0;
+
+               $userSQL = array();
+               if ( $userId ) {
+                       $userSQL = array( 'challenge_user_id_1' => $userId );
+               }
+
+               $s = $dbr->selectRow(
+                       'challenge',
+                       array( 'COUNT(*) AS count' ),
+                       $userSQL,
+                       __METHOD__
+               );
+
+               if ( $s !== false ) {
+                       $challengeCount = $s->count;
+               }
+
+               return $challengeCount;
+       }
+
+       /**
+        * Fetch everything we know about a challenge from the database when 
given
+        * a challenge identifier.
+        *
+        * @param int $id Challenge identifier
+        * @return array
+        */
+       public function getChallenge( $id ) {
+               $id = (int) $id; // paranoia!
+               $dbr = wfGetDB( DB_MASTER );
+               $sql = "SELECT {$dbr->tableName( 'challenge' )}.challenge_id AS 
id, challenge_user_id_1, challenge_username1, challenge_user_id_2, 
challenge_username2, challenge_info, challenge_description, 
challenge_event_date, challenge_status, challenge_winner_username, 
challenge_winner_user_id,
+                       challenge_win_terms, challenge_lose_terms, 
challenge_rate_score, challenge_rate_comment
+                       FROM {$dbr->tableName( 'challenge' )} LEFT JOIN 
{$dbr->tableName( 'challenge_rate' )}
+                               ON {$dbr->tableName( 'challenge_rate' 
)}.challenge_id = {$dbr->tableName( 'challenge' )}.challenge_id
+                       WHERE {$dbr->tableName( 'challenge' )}.challenge_id = 
{$id}";
+               $res = $dbr->query( $sql, __METHOD__ );
+
+               $challenge = array();
+               foreach ( $res as $row ) {
+                       $challenge[] = array(
+                               'id' => $row->id,
+                               'status' => $row->challenge_status,
+                               'user_id_1' => $row->challenge_user_id_1,
+                               'user_name_1' => $row->challenge_username1,
+                               'user_id_2' => $row->challenge_user_id_2,
+                               'user_name_2' => $row->challenge_username2,
+                               'info' => $row->challenge_info,
+                               'description' => $row->challenge_description,
+                               'date' => $row->challenge_event_date,
+                               'win_terms' => $row->challenge_win_terms,
+                               'lose_terms' => $row->challenge_lose_terms,
+                               'winner_user_id' => 
$row->challenge_winner_user_id,
+                               'winner_user_name' => 
$row->challenge_winner_username,
+                               'rating' => $row->challenge_rate_score,
+                               'rating_comment' => $row->challenge_rate_comment
+                       );
+               }
+
+               return $challenge[0];
+       }
+
+       /**
+        * Get the list of challenges that match the given conditions for a 
given
+        * user (via their user name).
+        *
+        * @param string $user_name User name
+        * @param int $status Challenge status code (or null for all challenges)
+        * @param int $limit SQL query LIMIT, i.e. get this many results
+        * @param int $page SQL query OFFSET, i.e. skip this many results
+        * @return array
+        */
+       public function getChallengeList( $user_name, $status = null, $limit = 
0, $page = 0 ) {
+               $limit_sql = $status_sql = $user_sql = '';
+               if ( $limit > 0 && is_int( $limit ) ) {
+                       $limitvalue = 0;
+                       if ( $page && is_int( $page ) ) {
+                               $limitvalue = $page * $limit - ( $limit );
+                       }
+                       $limit_sql = " LIMIT {$limitvalue},{$limit} ";
+               }
+
+               if ( $status != null && is_int( $status ) ) {
+                       $status_sql = " AND challenge_status = {$status}";
+               }
+               if ( $user_name ) {
+                       $user_id = User::idFromName( $user_name );
+                       $user_sql = " AND (challenge_user_id_1 = {$user_id} OR 
challenge_user_id_2 = {$user_id}) ";
+               }
+
+               $dbr = wfGetDB( DB_MASTER );
+               $sql = "SELECT {$dbr->tableName( 'challenge' )}.challenge_id AS 
id, challenge_user_id_1, challenge_username1, challenge_user_id_2, 
challenge_username2, challenge_info, challenge_description, 
challenge_event_date, challenge_status, challenge_winner_username, 
challenge_winner_user_id,
+                       challenge_win_terms, challenge_lose_terms, 
challenge_rate_score, challenge_rate_comment
+                       FROM {$dbr->tableName( 'challenge' )} LEFT JOIN 
{$dbr->tableName( 'challenge_rate' )} ON
+                               {$dbr->tableName( 'challenge_rate' 
)}.challenge_id = {$dbr->tableName( 'challenge' )}.challenge_id
+                       WHERE 1=1
+                       {$user_sql}
+                       {$status_sql}
+                       ORDER BY challenge_date DESC
+                       {$limit_sql}";
+
+               $res = $dbr->query( $sql, __METHOD__ );
+
+               $challenges = array();
+               foreach ( $res as $row ) {
+                       $challenges[] = array(
+                               'id' => $row->id,
+                               'status' => $row->challenge_status,
+                               'user_id_1' => $row->challenge_user_id_1,
+                               'user_name_1' => $row->challenge_username1,
+                               'user_id_2' => $row->challenge_user_id_2,
+                               'user_name_2' => $row->challenge_username2,
+                               'info' => $row->challenge_info,
+                               'description' => $row->challenge_description,
+                               'date' => $row->challenge_event_date,
+                               'win_terms' => $row->challenge_win_terms,
+                               'lose_terms' => $row->challenge_lose_terms,
+                               'winner_user_id' => 
$row->challenge_winner_user_id,
+                               'winner_user_name' => 
$row->challenge_winner_username,
+                               'rating' => $row->challenge_rate_score,
+                               'rating_comment' => $row->challenge_rate_comment
+                       );
+               }
+
+               return $challenges;
+       }
+
+       /**
+        * Get the challenge record for a given user ID.
+        *
+        * @param int $userId User ID
+        * @return string Wins, losses and ties separated by a dash
+        */
+       public static function getUserChallengeRecord( $userId ) {
+               $dbr = wfGetDB( DB_MASTER );
+               $s = $dbr->selectRow(
+                       'challenge_user_record',
+                       array( 'challenge_wins', 'challenge_losses', 
'challenge_ties' ),
+                       array( 'challenge_record_user_id' => $userId ),
+                       __METHOD__
+               );
+               if ( $s !== false ) {
+                       return $s->challenge_wins . '-' . $s->challenge_losses 
. '-' . $s->challenge_ties;
+               } else {
+                       return '0-0-0';
+               }
+       }
+
+       /**
+        * @param int $rateType
+        * @param int $userId
+        * @return int
+        */
+       public static function getUserFeedbackScoreByType( $rateType, $userId ) 
{
+               $dbr = wfGetDB( DB_MASTER );
+               return (int) $dbr->selectField(
+                       'challenge_rate',
+                       'COUNT(*) AS total',
+                       array(
+                               'challenge_rate_user_id' => $userId,
+                               'challenge_rate_score' => $rateType
+                       ),
+                       __METHOD__
+               );
+       }
+
+       /**
+        * Given a numeric status code, returns the corresponding human-readable
+        * status name.
+        *
+        * @param int $status Challenge status code
+        * @return string
+        */
+       public static function getChallengeStatusName( $status ) {
+               $out = '';
+               switch ( $status ) {
+                       case -1:
+                               $out .= wfMessage( 'challenge-status-rejected' 
)->plain();
+                               break;
+                       case -2:
+                               $out .= wfMessage( 'challenge-status-removed' 
)->plain();
+                               break;
+                       case 0:
+                               $out .= wfMessage( 'challenge-status-awaiting' 
)->plain();
+                               break;
+                       case 1:
+                               $out .= wfMessage( 
'challenge-status-in-progress' )->plain();
+                               break;
+                       case 3:
+                               $out .= wfMessage( 'challenge-status-completed' 
)->plain();
+                               break;
+               }
+               return $out;
+       }
+}
\ No newline at end of file
diff --git a/Challenge.php b/Challenge.php
new file mode 100644
index 0000000..212ea95
--- /dev/null
+++ b/Challenge.php
@@ -0,0 +1,129 @@
+<?php
+/**
+ * Challenge extension - allows challenging other users
+ *
+ * @file
+ * @ingroup Extensions
+ * @author Aaron Wright <aaron.wright{ at }gmail{ dot }com>
+ * @author David Pean <david.pean{ at }gmail{ dot }com>
+ * @author Jack Phoenix <j...@countervandalism.net>
+ * @version 1.0
+ * @link https://www.mediawiki.org/wiki/Extension:Challenge Documentation
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 
2.0 or later
+ */
+
+// Extension credits that show up on Special:Version
+$wgExtensionCredits['other'][] = array(
+       'name' => 'Challenge',
+       'version' => '1.0',
+       'author' => array( 'Aaron Wright', 'David Pean', 'Jack Phoenix' ),
+       'description' => 'Allows challenging other users',
+       'url' => 'https://www.mediawiki.org/wiki/Extension:Challenge'
+);
+
+// ResourceLoader support for MediaWiki 1.17+
+$commonCSSModuleProperties = array(
+       'localBasePath' => __DIR__,
+       'remoteExtPath' => 'Challenge',
+       'position' => 'top'
+);
+
+$wgResourceModules['ext.challenge.history'] = $commonCSSModuleProperties + 
array(
+       'styles' => 'resources/css/ext.challenge.history.css'
+);
+
+$wgResourceModules['ext.challenge.user'] = $commonCSSModuleProperties + array(
+       'styles' => 'resources/css/ext.challenge.user.css'
+);
+
+$wgResourceModules['ext.challenge.standings'] = $commonCSSModuleProperties + 
array(
+       'styles' => 'resources/css/ext.challenge.standings.css'
+);
+
+$wgResourceModules['ext.challenge.view'] = $commonCSSModuleProperties + array(
+       'styles' => 'resources/css/ext.challenge.view.css'
+);
+
+$wgResourceModules['ext.challenge.js.main'] = array(
+       'scripts' => 'resources/js/Challenge.js',
+       'messages' => array(
+               'challenge-js-event-required', 'challenge-js-date-required',
+               'challenge-js-description-required', 
'challenge-js-win-terms-required',
+               'challenge-js-lose-terms-required', 
'challenge-js-challenge-removed',
+               'challenge-js-accepted', 'challenge-js-rejected', 
'challenge-js-countered',
+               'challenge-js-winner-recorded', 'challenge-js-rating-submitted'
+       ),
+       'localBasePath' => __DIR__,
+       'remoteExtPath' => 'Challenge'
+);
+
+$wgResourceModules['ext.challenge.js.datevalidator'] = array(
+       'scripts' => 'resources/js/ValidateDate.js',
+       'messages' => array(
+               'challenge-js-error-date-format', 
'challenge-js-error-invalid-month',
+               'challenge-js-error-invalid-day', 
'challenge-js-error-invalid-year',
+               'challenge-js-error-invalid-date', 
'challenge-js-error-future-date',
+               'challenge-js-error-is-backwards'
+       ),
+       'localBasePath' => __DIR__,
+       'remoteExtPath' => 'Challenge'
+);
+
+$wgResourceModules['ext.challenge.js.datepicker'] = array(
+       'scripts' => 'resources/js/DatePicker.js',
+       'localBasePath' => __DIR__,
+       'remoteExtPath' => 'Challenge'
+);
+
+// Don't leak our temporary variable into global scope
+unset( $commonCSSModuleProperties );
+
+// New user right, required to pick the challenge winner via 
Special:ChallengeView
+$wgAvailableRights[] = 'challengeadmin';
+$wgGroupPermissions['sysop']['challengeadmin'] = true;
+
+// i18n
+$wgMessagesDirs['Challenge'] = __DIR__ . '/i18n';
+$wgExtensionMessagesFiles['ChallengeAliases'] = __DIR__ . 
'/Challenge.alias.php';
+
+// Classes to be autoloaded
+$wgAutoloadClasses['Challenge'] = __DIR__ . '/Challenge.class.php';
+$wgAutoloadClasses['ChallengeAction'] = __DIR__ . '/ChallengeAction.php';
+$wgAutoloadClasses['ChallengeHistory'] = __DIR__ . '/ChallengeHistory.php';
+$wgAutoloadClasses['ChallengeStandings'] = __DIR__ . '/ChallengeStandings.php';
+$wgAutoloadClasses['ChallengeUser'] = __DIR__ . '/ChallengeUser.php';
+$wgAutoloadClasses['ChallengeView'] = __DIR__ . '/ChallengeView.php';
+
+// New special pages
+$wgSpecialPages['ChallengeAction'] = 'ChallengeAction';
+$wgSpecialPages['ChallengeHistory'] = 'ChallengeHistory';
+$wgSpecialPages['ChallengeStandings'] = 'ChallengeStandings';
+$wgSpecialPages['ChallengeUser'] = 'ChallengeUser';
+$wgSpecialPages['ChallengeView'] = 'ChallengeView';
+
+/**
+ * Adds the three new required database tables into the database when the
+ * user runs /maintenance/update.php (the core database updater script).
+ *
+ * @param DatabaseUpdater $updater
+ * @return bool
+ */
+$wgHooks['LoadExtensionSchemaUpdates'][] = function( $updater ) {
+       $dir = __DIR__;
+
+       $dbType = $updater->getDB()->getType();
+
+       $filename = 'challenge.sql';
+       // For non-MySQL/MariaDB/SQLite DBMSes, use the appropriately named file
+       /*
+       if ( !in_array( $dbType, array( 'mysql', 'sqlite' ) ) ) {
+               $filename = "schema.{$dbType}.sql";
+       }
+       */
+
+       $updater->addExtensionUpdate( array( 'addTable', 'challenge', 
"{$dir}/{$filename}", true ) );
+       $updater->addExtensionUpdate( array( 'addTable', 'challenge_rate', 
"{$dir}/{$filename}", true ) );
+       $updater->addExtensionUpdate( array( 'addTable', 
'challenge_user_record', "{$dir}/{$filename}", true ) );
+
+       return true;
+};
\ No newline at end of file
diff --git a/ChallengeAction.php b/ChallengeAction.php
new file mode 100644
index 0000000..8c4ffe5
--- /dev/null
+++ b/ChallengeAction.php
@@ -0,0 +1,68 @@
+<?php
+
+class ChallengeAction extends UnlistedSpecialPage {
+
+       public function __construct() {
+               parent::__construct( 'ChallengeAction' );
+       }
+
+       /**
+        * Show the special page
+        *
+        * @param mixed $par Parameter passed to the page or null
+        */
+       public function execute( $par ) {
+               $request = $this->getRequest();
+               $c = new Challenge();
+
+               switch ( $request->getVal( 'action' ) ) {
+                       case 1:
+                               $c->updateChallengeStatus(
+                                       $request->getVal( 'id' ),
+                                       $request->getVal( 'status' )
+                               );
+                               break;
+                       case 2:
+                               //if ( $this->getUser()->isAllowed( 
'challengeadmin' ) ) {
+                                       $c->updateChallengeWinner(
+                                               $request->getVal( 'id' ),
+                                               $request->getVal( 'userid' )
+                                       );
+                                       $c->updateChallengeStatus( 
$request->getVal( 'id' ), 3 );
+                               //}
+                               break;
+                       case 3:
+                               // Update social stats for both users involved 
in challenge
+                               $stats = new UserStatsTrack(
+                                       1,
+                                       $request->getVal( 'loser_userid' ),
+                                       $request->getVal( 'loser_username' )
+                               );
+                               if ( $request->getVal( 'challenge_rate' ) == 1 
) {
+                                       $stats->incStatField( 
'challenges_rating_positive' );
+                               }
+                               if ( $request->getVal( 'challenge_rate' ) == -1 
) {
+                                       $stats->incStatField( 
'challenges_rating_negative' );
+                               }
+
+                               $dbw = wfGetDB( DB_MASTER );
+                               $dbw->insert(
+                                       'challenge_rate',
+                                       array(
+                                               'challenge_id' => 
$request->getVal( 'id' ),
+                                               
'challenge_rate_submitter_user_id' => $this->getUser()->getId(),
+                                               
'challenge_rate_submitter_username' => $this->getUser()->getName(),
+                                               'challenge_rate_user_id' => 
$request->getVal( 'loser_userid' ),
+                                               'challenge_rate_username' => 
$request->getVal( 'loser_username' ),
+                                               'challenge_rate_date' => 
$dbw->timestamp(),
+                                               'challenge_rate_score' => 
$request->getVal( 'challenge_rate' ),
+                                               'challenge_rate_comment' => 
$request->getVal( 'rate_comment' )
+                                       ),
+                                       __METHOD__
+                               );
+                               break;
+               }
+
+               $this->getOutput()->setArticleBodyOnly( true );
+       }
+}
\ No newline at end of file
diff --git a/ChallengeHistory.php b/ChallengeHistory.php
new file mode 100644
index 0000000..e6e97f5
--- /dev/null
+++ b/ChallengeHistory.php
@@ -0,0 +1,248 @@
+<?php
+
+class ChallengeHistory extends SpecialPage {
+
+       public function __construct() {
+               parent::__construct( 'ChallengeHistory' );
+       }
+
+       /**
+        * Under which header this special page is listed in 
Special:SpecialPages?
+        *
+        * @return string
+        */
+       protected function getGroupName() {
+               return 'users';
+       }
+
+       private function displayUserHeader( $user_name, $userId ) {
+               //$avatar = new wAvatar( $userId, 'l' );
+               $pos = Challenge::getUserFeedbackScoreByType( 1, $userId );
+               $neg = Challenge::getUserFeedbackScoreByType( -1, $userId );
+               $neu = Challenge::getUserFeedbackScoreByType( 0, $userId );
+               $total = ( $pos + $neg + $neu );
+               $percent = 0;
+               if ( $pos ) {
+                       $percent = $pos / $total * 100;
+               }
+
+               $out = '<b>' . $this->msg( 'challengehistory-overall' 
)->plain() . '</b>: (' .
+                       Challenge::getUserChallengeRecord( $userId ) . ')<br 
/><br />';
+               $out .= '<b>' . $this->msg( 'challengehistory-ratings-loser' 
)->plain() . '</b>: <br />';
+               $out .= '<span class="challenge-rate-positive">' .
+                       $this->msg( 'challengehistory-positive' )->plain() . 
'</span>: ' . $pos . ' (' . $percent . '%)<br />';
+               $out .= '<span class="challenge-rate-negative">' .
+                       $this->msg( 'challengehistory-negative' )->plain() . 
'</span>: ' . $neg . '<br />';
+               $out .= '<span class="challenge-rate-neutral">' .
+                       $this->msg( 'challengehistory-neutral' )->plain() . 
'</span>: ' . $neu . '<br /><br />';
+               return $out;
+       }
+
+       /**
+        * Show the special page
+        *
+        * @param mixed $par Parameter passed to the page or null
+        */
+       public function execute( $par ) {
+               global $wgExtensionAssetsPath;
+
+               $request = $this->getRequest();
+               $imgPath = $wgExtensionAssetsPath . 
'/Challenge/resources/images/';
+               $spImgPath = $wgExtensionAssetsPath . '/SocialProfile/images/';
+               $u = $request->getVal( 'user', $par );
+
+               $this->getOutput()->addModuleStyles( 'ext.challenge.history' );
+
+               $out = $standings_link = '';
+               if ( $u ) {
+                       $userTitle = Title::newFromDBkey( $u );
+                       if ( $userTitle ) {
+                               $userId = User::idFromName( 
$userTitle->getText() );
+                       } else {
+                               // invalid user
+                               // @todo FIXME: in this case, what is $userId 
when it gets passed
+                               // to displayUserHeader() below?
+                       }
+
+                       $this->getOutput()->setPageTitle(
+                               $this->msg( 'challengehistory-users-history',
+                                       $userTitle->getText() )->parse()
+                       );
+                       $out .= $this->displayUserHeader( 
$userTitle->getText(), $userId );
+               } else {
+                       $this->getOutput()->setPageTitle( $this->msg( 
'challengehistory-recentchallenges' ) );
+                       $standings_link = " - <img 
src=\"{$imgPath}userpageIcon.png\" alt=\"\" /> ";
+                       $standings_link .= Linker::link(
+                               SpecialPage::getTitleFor( 'ChallengeStandings' 
),
+                               $this->msg( 'challengehistory-view-standings' 
)->plain()
+                       );
+               }
+
+               $challenge_link = SpecialPage::getTitleFor( 'ChallengeUser' );
+               $status = (int) $request->getVal( 'status' );
+               $out .= '
+               <div class="challenge-nav">
+                       <div class="challenge-history-filter">' . $this->msg( 
'challengehistory-filter' )->plain();
+               // @todo CHECKME: is this secure enough?
+               $sanitizedUser = htmlspecialchars( $u, ENT_QUOTES );
+               $out .= "<select name=\"status-filter\" 
data-username=\"{$sanitizedUser}\">
+                               <option value='' " . ( $status == '' && strlen( 
$status ) == 0 ? ' selected="selected"' : '' ) . '>' . $this->msg( 
'challengehistory-all' )->plain() . "</option>
+                               <option value=0 " . ( $status == 0 && strlen( 
$status ) == 1 ? ' selected="selected"' : '' ) . '>' . $this->msg( 
'challengehistory-awaiting' )->plain() . '</option>
+                               <option value="1"' . ( $status == 1 ? ' 
selected="selected"' : '' ) . '>' . $this->msg( 'challengehistory-inprogress' 
)->plain() . '</option>
+                               <option value="-1"' . ( $status == -1 ? ' 
selected="selected"' : '' ) . '>' . $this->msg( 'challengehistory-rejected' 
)->plain() . '</option>
+                               <option value="3"' . ( $status == 3 ? ' 
selected="selected"' : '' ) . '>' . $this->msg( 'challengehistory-completed' 
)->plain() . "</option>
+                       </select>
+                       </div>
+                       <div class=\"challenge-link\">
+                               <img src=\"{$spImgPath}challengeIcon.png\" 
alt=\"\" /> ";
+               if ( $u ) {
+                       $msg = $this->msg( 'challengehistory-challenge-user', 
$userTitle->getText() )->parse();
+               } else {
+                       $msg = $this->msg( 'challengehistory-challenge-someone' 
)->plain();
+               }
+               $out .= Linker::link(
+                       $challenge_link,
+                       $msg,
+                       array(),
+                       ( ( $u ) ? array( 'user' => $u ) : array() )
+               );
+               $out .= $this->msg( 'word-separator' )->escaped();
+               $out .= "{$standings_link}
+                       </div>
+                       <div class=\"cleared\"></div>
+               </div>
+
+               <table class=\"challenge-history-table\">
+                       <tr>
+                               <td class=\"challenge-history-header\">" . 
$this->msg( 'challengehistory-event' )->plain() . '</td>
+                               <td class="challenge-history-header">' . 
$this->msg( 'challengehistory-challenger-desc' )->plain() . '</td>
+                               <td class="challenge-history-header">' . 
$this->msg( 'challengehistory-challenger' )->plain() . '</td>
+                               <td class="challenge-history-header">' . 
$this->msg( 'challengehistory-target' )->plain() . '</td>
+                               <td class="challenge-history-header">' . 
$this->msg( 'challengehistory-status' )->plain() . '</td>
+                       </tr>';
+
+               $page = (int) $request->getVal( 'page', 1 );
+               $perPage = 25;
+
+               $c = new Challenge();
+               $challengeList = $c->getChallengeList(
+                       $u,
+                       $status,
+                       $perPage,
+                       $page
+               );
+               $totalChallenges = $c->getChallengeCount();
+
+               if ( $challengeList ) {
+                       foreach ( $challengeList as $challenge ) {
+                               // Set up avatars and wiki titles for challenge 
and target
+                               $avatar1 = new wAvatar( 
$challenge['user_id_1'], 's' );
+                               $avatar2 = new wAvatar( 
$challenge['user_id_2'], 's' );
+
+                               $title1 = Title::makeTitle( NS_USER, 
$challenge['user_name_1'] );
+                               $title2 = Title::makeTitle( NS_USER, 
$challenge['user_name_2'] );
+
+                               // Set up titles for pages used in table
+                               $challenge_view_title = 
SpecialPage::getTitleFor( 'ChallengeView' );
+                               $challengeViewLink = Linker::link(
+                                       $challenge_view_title,
+                                       htmlspecialchars( $challenge['info'] . 
' [' . $challenge['date'] . ']' ),
+                                       array(),
+                                       array( 'id' => $challenge['id'] )
+                               );
+
+                               $av1 = $avatar1->getAvatarURL( array( 'align' 
=> 'absmiddle' ) ); // @todo FIXME: invalid HTML5
+                               $av2 = $avatar2->getAvatarURL( array( 'align' 
=> 'absmiddle' ) ); // @todo FIXME: invalid HTML5
+                               $winnerSymbol = Html::element(
+                                       'img',
+                                       array(
+                                               'src' => $imgPath . 
'winner-check.gif',
+                                               'alt' => '',
+                                               'align' => 'absmiddle' // @todo 
FIXME: invalid HTML5
+                                       )
+                               );
+
+                               $out .= "<tr>
+                                       <td 
class=\"challenge-data\">{$challengeViewLink}</td>
+                                       <td class=\"challenge-data 
challenge-data-description\">{$challenge['description']}</td>
+                                       <td class=\"challenge-data\">{$av1}";
+                               $out .= Linker::link(
+                                       $title1,
+                                       $challenge['user_name_1']
+                               );
+                               $out .= $this->msg( 'word-separator' 
)->escaped();
+                               if ( $challenge['winner_user_id'] == 
$challenge['user_id_1'] ) {
+                                       $out .= $winnerSymbol;
+                               }
+                               $out .= "</td>
+                                       <td class=\"challenge-data\">{$av2}";
+                               $out .= Linker::link(
+                                       $title2,
+                                       $challenge['user_name_2']
+                               );
+                               $out .= $this->msg( 'word-separator' 
)->escaped();
+                               if ( $challenge['winner_user_id'] == 
$challenge['user_id_2'] ) {
+                                       $out .= $winnerSymbol;
+                               }
+                               $out .= "</td>
+                                       <td 
class=\"challenge-data\">{$c->getChallengeStatusName( $challenge['status'] 
)}</td>
+                               </tr>";
+                       }
+               } else {
+                       $out .= '<tr><td class="challenge-history-empty"><br 
/>';
+                       $out .= $this->msg( 'challengehistory-empty' )->parse();
+                       $out .= '</td></tr>';
+               }
+
+               $out .= '</table>';
+
+               // Build next/prev navigation
+               $numOfPages = $totalChallenges / $perPage;
+
+               if ( $numOfPages > 1 && !$u ) {
+                       $challenge_history_title = SpecialPage::getTitleFor( 
'ChallengeHistory' );
+                       $out .= '<div class="page-nav">';
+                       if ( $page > 1 ) {
+                               $out .= Linker::link(
+                                       $challenge_history_title,
+                                       $this->msg( 'challengehistory-prev' 
)->plain(),
+                                       array(),
+                                       array( 'user' => $user_name, 'page' => 
( $page - 1 ) )
+                               ) . $this->msg( 'word-separator' )->escaped();
+                       }
+
+                       if ( ( $total % $perPage ) != 0 ) {
+                               $numOfPages++;
+                       }
+                       if ( $numOfPages >= 9 ) {
+                               $numOfPages = 9 + $page;
+                       }
+
+                       for ( $i = 1; $i <= $numOfPages; $i++ ) {
+                               if ( $i == $page ) {
+                                       $out .= ( $i . ' ' );
+                               } else {
+                                       $out .= Linker::link(
+                                               $challenge_history_title,
+                                               $i,
+                                               array(),
+                                               array( 'user' => $user_name, 
'page' => $i )
+                                       ) . $this->msg( 'word-separator' 
)->escaped();
+                               }
+                       }
+
+                       if ( ( $total - ( $perPage * $page ) ) > 0 ) {
+                               $out .= $this->msg( 'word-separator' 
)->escaped() . Linker::link(
+                                       $challenge_history_title,
+                                       $this->msg( 'challengehistory-next' 
)->plain(),
+                                       array(),
+                                       array( 'user' => $user_name, 'page' => 
( $page + 1 ) )
+                               );
+                       }
+                       $out .= '</div>';
+               }
+
+               $this->getOutput()->addModules( 'ext.challenge.js.main' );
+               $this->getOutput()->addHTML( $out );
+       }
+}
\ No newline at end of file
diff --git a/ChallengeStandings.php b/ChallengeStandings.php
new file mode 100644
index 0000000..1961b8a
--- /dev/null
+++ b/ChallengeStandings.php
@@ -0,0 +1,84 @@
+<?php
+
+class ChallengeStandings extends SpecialPage {
+
+       public function __construct() {
+               parent::__construct( 'ChallengeStandings' );
+       }
+
+       /**
+        * Under which header this special page is listed in 
Special:SpecialPages?
+        *
+        * @return string
+        */
+       protected function getGroupName() {
+               return 'users';
+       }
+
+       /**
+        * Show the special page
+        *
+        * @param mixed $par Parameter passed to the page or null
+        */
+       public function execute( $par ) {
+               $this->getOutput()->setPageTitle( $this->msg( 
'challengestandings-title' ) );
+
+               $out = '<table class="challenge-standings-table">
+                       <tr>
+                               <td class="challenge-standings-title">#</td>
+                               <td class="challenge-standings-title">' . 
$this->msg( 'challengestandings-user' )->plain() . '</td>
+                               <td class="challenge-standings-title">' . 
$this->msg( 'challengestandings-w' )->plain() . '</td>
+                               <td class="challenge-standings-title">' . 
$this->msg( 'challengestandings-l' )->plain() . '</td>
+                               <td class="challenge-standings-title">' . 
$this->msg( 'challengestandings-t' )->plain() . '</td>
+                               <td class="challenge-standings-title">%</td>
+                               <td class="challenge-standings-title"></td>
+                       </tr>';
+
+               $dbr = wfGetDB( DB_SLAVE );
+               $sql = "SELECT challenge_record_username, 
challenge_record_user_id, challenge_wins, challenge_losses, challenge_ties, 
(challenge_wins / (challenge_wins + challenge_losses + challenge_ties) ) AS 
winning_percentage FROM {$dbr->tableName( 'challenge_user_record' )} ORDER BY 
(challenge_wins / (challenge_wins + challenge_losses + challenge_ties) ) DESC, 
challenge_wins DESC LIMIT 0,25";
+               $res = $dbr->query( $sql, __METHOD__ );
+               $x = 1;
+
+               foreach ( $res as $row ) {
+                       $avatar1 = new wAvatar( $row->challenge_record_user_id, 
's' );
+                       $out .= '<tr>
+                               <td class="challenge-standings">' . $x . '</td>
+                               <td class="challenge-standings">';
+                       $out .= $avatar1->getAvatarURL( array( 'align' => 
'absmiddle' ) ); // @todo FIXME: invalid HTML5
+                       $out .= Linker::link(
+                               SpecialPage::getTitleFor( 'ChallengeHistory' ),
+                               $row->challenge_record_username,
+                               array( 'class' => 
'challenge-standings-history-link' ),
+                               array( 'user' => 
$row->challenge_record_username )
+                       );
+                       $out .= $this->msg( 'word-separator' )->escaped();
+                       $out .= $user1Icon . '</td>';
+
+                       $out .= '<td class="challenge-standings">' . 
$row->challenge_wins . '</td>
+                                       <td class="challenge-standings">' . 
$row->challenge_losses . '</td>
+                                       <td class="challenge-standings">' . 
$row->challenge_ties . '</td>
+                                       <td class="challenge-standings">' .
+                                               // @todo FIXME: not 
i18n-compatible, should use $this->getLanguage()->formatNum( 
$row->winning_percentage ) or something instead...
+                                               str_replace( '.0', '.', 
number_format( $row->winning_percentage, 3 ) ) .
+                                       '</td>';
+
+                       if ( $row->challenge_record_username != 
$this->getUser()->getName() ) {
+                               $out .= '<td class="challenge-standings">';
+                               $out .= Linker::link(
+                                       SpecialPage::getTitleFor( 
'ChallengeUser' ),
+                                       $this->msg( 
'challengestandings-challengeuser' )->plain(),
+                                       array( 'class' => 
'challenge-standings-user-link' ),
+                                       array( 'user' => 
$row->challenge_record_username )
+                               );
+                               $out .= '</td>';
+                       }
+
+                       $out .= '</td></tr>';
+                       $x++;
+               }
+
+               $out .= '</table>';
+
+               $this->getOutput()->addHTML( $out );
+       }
+}
\ No newline at end of file
diff --git a/ChallengeUser.php b/ChallengeUser.php
new file mode 100644
index 0000000..fe5f9fc
--- /dev/null
+++ b/ChallengeUser.php
@@ -0,0 +1,181 @@
+<?php
+
+class ChallengeUser extends SpecialPage {
+
+       public function __construct() {
+               parent::__construct( 'ChallengeUser' );
+       }
+
+       /**
+        * Under which header this special page is listed in 
Special:SpecialPages?
+        *
+        * @return string
+        */
+       protected function getGroupName() {
+               return 'users';
+       }
+
+       /**
+        * Show the special page
+        *
+        * @param mixed $par Parameter passed to the page or null
+        */
+       public function execute( $par ) {
+               $output = $this->getOutput();
+               $request = $this->getRequest();
+               $user = $this->getUser();
+
+               // Add CSS & JS via ResourceLoader
+               $output->addModuleStyles( 'ext.challenge.user' );
+               $output->addModules( array(
+                       'ext.challenge.js.main',
+                       'ext.challenge.js.datepicker'
+               ) );
+
+               $userTitle = Title::newFromDBkey( $request->getVal( 'user', 
$par ) );
+               if ( !$userTitle ) {
+                       $output->addHTML( $this->displayFormNoUser() );
+                       return false;
+               }
+
+               $this->user_name_to = $userTitle->getText();
+               $this->user_id_to = User::idFromName( $this->user_name_to );
+
+               if ( $user->getId() == $this->user_id_to ) {
+                       $output->setPageTitle( $this->msg( 
'challengeuser-error-page-title' ) );
+                       $output->addHTML( $this->msg( 'challengeuser-self' 
)->plain() );
+               } elseif ( $this->user_id_to == 0 ) {
+                       $output->setPageTitle( $this->msg( 
'challengeuser-error-page-title' ) );
+                       $output->addHTML( $this->msg( 'challengeuser-nouser' 
)->plain() );
+               } elseif ( $user->getId() == 0 ) {
+                       $output->setPageTitle( $this->msg( 
'challengeuser-error-page-title' ) );
+                       $output->addHTML( $this->msg( 'challengeuser-login' 
)->plain() );
+               } else {
+                       if ( $request->wasPosted() && 
$_SESSION['alreadysubmitted'] === false ) {
+                               $_SESSION['alreadysubmitted'] = true;
+                               $c = new Challenge();
+                               $c->addChallenge(
+                                       $this->user_name_to,
+                                       $request->getVal( 'info' ),
+                                       $request->getVal( 'date' ),
+                                       $request->getVal( 'description' ),
+                                       $request->getVal( 'win' ),
+                                       $request->getVal( 'lose' )
+                               );
+
+                               $output->setPageTitle(
+                                       $this->msg( 
'challengeuser-challenge-sent-title',
+                                               $this->user_name_to )->parse()
+                               );
+
+                               $out = '<div class="challenge-links">';
+                                       //$out .= "<a 
href=\"index.php?title=User:{$this->user_name_to}\">< {$this->user_name_to}'s 
User Page</a>";
+                                       // $out .= " - <a 
href=\"index.php?title=Special:ViewGifts&user={$this->user_name_to}\">View All 
of {$this->user_name_to}'s Gifts</a>";
+                               if ( $this->getUser()->isLoggedIn() ) {
+                                       // $out .= " - <a 
href=\"index.php?title=Special:ViewGifts&user={$wgUser->getName()}\">View All 
of Your Gifts</a>";
+                               }
+                               $out .= '</div>';
+
+                               $out .= '<div class="challenge-sent-message">';
+                               $out .= $this->msg( 'challengeuser-sent', 
$this->user_name_to )->parse();
+                               $out .= '</div>';
+
+                               $out .= '<div class="cleared"></div>';
+
+                               $output->addHTML( $out );
+                       } else {
+                               $_SESSION['alreadysubmitted'] = false;
+                               $output->addHTML( $this->displayForm() );
+                       }
+               }
+       }
+
+       function displayFormNoUser() {
+               global $wgFriendingEnabled;
+
+               $this->getOutput()->setPageTitle( $this->msg(
+                       'challengeuser-info-title-no-user' )->plain() );
+
+               // @todo FIXME: rename form & HTML classes/IDs
+               $output = '<form action="" method="get" 
enctype="multipart/form-data" name="gift">
+               <input type="hidden" name="title" value="' . htmlspecialchars( 
$this->getRequest()->getVal( 'title' ), ENT_QUOTES ) . '" />';
+
+               $output .= '<div class="give-gift-message">';
+               $output .= $this->msg( 'challengeuser-info-body-no-user' 
)->plain();
+               $output .= '</div>';
+
+               if ( $wgFriendingEnabled ) {
+                       $rel = new UserRelationship( 
$this->getUser()->getName() );
+                       $friends = $rel->getRelationshipList( 1 );
+                       if ( $friends ) {
+                               $output .= '<div class="give-gift-title">';
+                               $output .= $this->msg( 
'challengeuser-select-friend-from-list' )->plain();
+                               $output .= '</div>
+                               <div class="give-gift-selectbox">
+                               <select id="challenge-user-selector">';
+                               $output .= '<option value="#" 
selected="selected">';
+                               $output .= $this->msg( 
'challengeuser-select-friend' )->plain();
+                               $output .= '</option>';
+                               foreach ( $friends as $friend ) {
+                                       $output .= Html::element(
+                                               'option',
+                                               array( 'value' => 
$friend['user_name'] ),
+                                               $friend['user_name']
+                                       );
+                               }
+                               $output .= '</select>
+                               </div>';
+                       }
+               }
+
+               $output .= '<p class="challenge-user-or">';
+               $output .= $this->msg( 'challengeuser-or' )->plain();
+               $output .= '</p>';
+               $output .= '<div class="give-gift-title">';
+               $output .= $this->msg( 'challengeuser-type-username' )->plain();
+               $output .= '</div>';
+               $output .= '<div class="give-gift-textbox">
+                       <input type="text" width="85" name="user" value="" />
+                       <input class="site-button" type="button" value="' .
+                               $this->msg( 'challengeuser-start-button' 
)->plain() .
+                               '" onclick="document.gift.submit()" />
+               </div>';
+
+               return $output;
+       }
+
+       /**
+        * Displays the "challenge a user" form
+        *
+        * @return string Generated HTML for the challenge form
+        */
+       function displayForm() {
+               $this->getOutput()->setPageTitle(
+                       $this->msg( 'challengeuser-title-user',
+                               $this->user_name_to )->parse()
+               );
+
+               $user_title = Title::makeTitle( NS_USER, $this->user_name_to );
+               $challenge_history_title = SpecialPage::getTitleFor( 
'ChallengeHistory' );
+               $avatar = new wAvatar( $this->user_id_to, 'l' );
+
+               $pos = Challenge::getUserFeedbackScoreByType( 1, 
$this->user_id_to );
+               $neg = Challenge::getUserFeedbackScoreByType( -1, 
$this->user_id_to );
+               $neu = Challenge::getUserFeedbackScoreByType( 0, 
$this->user_id_to );
+               $total = ( $pos + $neg + $neu );
+
+               require_once( 'templates/challengeuser.tmpl.php' );
+               $template = new ChallengeUserTemplate();
+
+               $template->set( 'pos', $pos );
+               $template->set( 'neg', $neg );
+               $template->set( 'neu', $neu );
+               $template->set( 'total', $total );
+               $template->setRef( 'class', $this );
+               $template->setRef( 'user_title', $user_title );
+               $template->setRef( 'challenge_history_title', 
$challenge_history_title );
+               $template->setRef( 'avatar', $avatar );
+
+               return $template->getHTML();
+       }
+}
\ No newline at end of file
diff --git a/ChallengeView.php b/ChallengeView.php
new file mode 100644
index 0000000..97409e3
--- /dev/null
+++ b/ChallengeView.php
@@ -0,0 +1,169 @@
+<?php
+
+class ChallengeView extends SpecialPage {
+
+       public function __construct() {
+               parent::__construct( 'ChallengeView' );
+       }
+
+       /**
+        * Under which header this special page is listed in 
Special:SpecialPages?
+        *
+        * @return string
+        */
+       protected function getGroupName() {
+               return 'users';
+       }
+
+       /**
+        * Show the special page
+        *
+        * @param mixed $par Parameter (challenge ID) passed to the page or null
+        */
+       public function execute( $par ) {
+               $this->getOutput()->setPageTitle( $this->msg( 'challengeview' ) 
);
+
+               $id = (int) $this->getRequest()->getVal( 'id', $par );
+               if ( $id == '' ) {
+                       $this->getOutput()->addHTML( $this->msg( 
'challengeview-nochallenge' )->plain() );
+               } else {
+                       $this->getOutput()->addHTML( $this->displayChallenge( 
$id ) );
+               }
+       }
+
+       private function displayChallenge( $id ) {
+               $this->getOutput()->addModuleStyles( 'ext.challenge.view' );
+               $this->getOutput()->addModules( 'ext.challenge.js.main' );
+
+               $c = new Challenge();
+               $challenge = $c->getChallenge( $id );
+
+               $u = $this->getUser();
+               $avatar1 = new wAvatar( $challenge['user_id_1'], 'l' );
+               $avatar2 = new wAvatar( $challenge['user_id_2'], 'l' );
+               $title1 = Title::makeTitle( NS_USER, $challenge['user_name_1'] 
);
+               $title2 = Title::makeTitle( NS_USER, $challenge['user_name_2'] 
);
+
+               require_once( 'templates/challengeview.tmpl.php' );
+               $template = new ChallengeViewTemplate();
+
+               $template->setRef( 'c', $c );
+               $template->set( 'challenge', $challenge );
+               $template->setRef( 'avatar1', $avatar1 );
+               $template->setRef( 'avatar2', $avatar2 );
+               $template->setRef( 'title1', $title1 );
+               $template->setRef( 'title2', $title2 );
+               $template->setRef( 'user', $u );
+
+               $out = '';
+               switch ( $challenge['status'] ) {
+                       case 0:
+                               if ( $this->getUser()->getId() != 
$challenge['user_id_2'] ) {
+                                       $out .= $this->msg( 
'challengeview-acceptance' )->plain();
+                               } else {
+                                       $out .= $this->msg( 
'challengeview-sent-to-you' )->plain();
+                                       $out .= '<br /><br />
+                                       <select id="challenge_action">
+                                               <option value="1">' . 
$this->msg( 'challengeview-accept' )->plain() . '</option>
+                                               <option value="-1">' . 
$this->msg( 'challengeview-reject' )->plain() . '</option>
+                                               <option value="2">' . 
$this->msg( 'challengeview-counterterms' )->plain() . "</option>
+                                       </select>
+                                       <input type=\"hidden\" id=\"status\" 
value=\"{$challenge['status']}\" />
+                                       <input type=\"hidden\" 
id=\"challenge_id\" value=\"{$challenge['id']}\" />
+                                       <input type=\"button\" 
class=\"challenge-response-button site-button\" value=\"" . $this->msg( 
'challengeview-submit-button' )->plain() .
+                                               '" />';
+                               }
+                               break;
+                       case 1:
+                               if (
+                                       !$this->getUser()->isAllowed( 
'challengeadmin' ) ||
+                                       $challenge['user_id_1'] == 
$this->getUser()->getId() ||
+                                       $challenge['user_id_2'] == 
$this->getUser()->getId()
+                               )
+                               {
+                                       $out .= $this->msg( 
'challengeview-inprogress' )->plain();
+                               } else {
+                                       $out .= $this->msg( 
'challengeview-admintext' )->escaped();
+                                       $out .= "<br /><br />
+                                       <select id=\"challenge_winner_userid\">
+                                               <option 
value=\"{$challenge['user_id_1']}\">{$challenge['user_name_1']}</option>
+                                               <option 
value=\"{$challenge['user_id_2']}\">{$challenge['user_name_2']}</option>
+                                               <option value=\"-1\">";
+                                       $out .= $this->msg( 
'challengeview-push' )->plain();
+                                       $out .= "</option>
+                                       </select>
+                                       <input type=\"hidden\" id=\"status\" 
value=\"{$challenge['status']}\" />
+                                       <input type=\"hidden\" 
id=\"challenge_id\" value=\"{$challenge['id']}\" />
+                                       <input type=\"button\" 
class=\"challenge-approval-button site-button\" value=\"" .
+                                               $this->msg( 
'challengeview-submit-button' )->plain() .
+                                       '" />';
+                               }
+                               break;
+                       case -1:
+                               $out .= $this->msg( 'challengeview-rejected' 
)->plain();
+                               break;
+                       case -2:
+                               $out .= $this->msg( 'challengeview-removed' 
)->plain();
+                               break;
+                       case 3:
+                               if ( $challenge['winner_user_id'] != -1 ) {
+                                       $out .= $this->msg( 
'challengeview-won-by', $challenge['winner_user_name'] )->escaped();
+                                       $out .= '<br /><br />';
+                                       if ( $challenge['rating'] ) {
+                                               $out .= '<span 
class="challenge-title">';
+                                               $out .= $this->msg( 
'challengeview-rating' )->plain();
+                                               $out .= '</span><br />';
+                                               $out .= $this->msg( 
'challengeview-by', $challenge['winner_user_name'] )->escaped();
+                                               $out .= '<br /><br />' . 
$this->msg( 'challengeview-rating2' )->escaped() .
+                                                       " <span 
class=\"challenge-rating-{$c->rating_names[$challenge['rating']]}\">{$c->rating_names[$challenge['rating']]}</span>
+                                                       <br />";
+                                               $out .= $this->msg( 
'challengeview-comment', $challenge['rating_comment'] )->escaped();
+                                       } else {
+                                               if ( $this->getUser()->getId() 
== $challenge['winner_user_id'] ) {
+                                                       $out .= '<span 
class="challenge-title">';
+                                                       $out .= $this->msg( 
'challengeview-rating' )->plain();
+                                                       $out .= '</span><br />
+                                                               <span 
class="challenge-won">';
+                                                       $out .= $this->msg( 
'challengeview-you-won' )->plain();
+                                                       $out .= '</span><br 
/><br />
+                                                               <span 
class="challenge-form">';
+                                                       $out .= $this->msg( 
'challengeview-rateloser' )->plain();
+                                                       $out .= '</span><br />
+                                                               <select 
id="challenge_rate">
+                                                                       <option 
value="1">' . $this->msg( 'challengeview-positive' )->plain() . '</option>
+                                                                       <option 
value="-1">' . $this->msg( 'challengeview-negative' )->plain() . '</option>
+                                                                       <option 
value="0">' . $this->msg( 'challengeview-neutral' )->plain() . "</option>
+                                                               </select>
+                                                               <input 
type=\"hidden\" id=\"status\" value=\"{$challenge['status']}\" />
+                                                               <input 
type=\"hidden\" id=\"challenge_id\" value=\"{$challenge['id']}\" />";
+                                                       if ( 
$challenge['winner_user_id'] == $challenge['user_id_1'] ) {
+                                                               $loser_id = 
$challenge['user_id_2'];
+                                                               $loser_username 
= $challenge['user_name_2'];
+                                                       } else {
+                                                               $loser_id = 
$challenge['user_id_1'];
+                                                               $loser_username 
= $challenge['user_name_1'];
+                                                       }
+                                                       $out .= "<input 
type=\"hidden\" id=\"loser_userid\" value=\"{$loser_id}\" />
+                                                               <input 
type=\"hidden\" id=\"loser_username\" value=\"{$loser_username}\" />
+                                                       <br /><br /><span 
class=\"challenge-form\">";
+                                                       $out .= $this->msg( 
'challengeview-additionalcomments' )->plain();
+                                                       $out .= '</span><br />
+                                                               <textarea 
class="createbox" rows="2" cols="50" id="rate_comment"></textarea><br /><br />
+                                                               <input 
type="button" class="challenge-rate-button site-button" value="' .
+                                                                       
$this->msg( 'challengeview-submit-button' )->plain() .
+                                                                       '" />';
+                                               } else {
+                                                       $out .= $this->msg( 
'challengeview-notyetrated' )->plain();
+                                               }
+                                       }
+                               } else {
+                                       $out .= $this->msg( 
'challengeview-was-push' )->plain() . '<br /><br />';
+                               }
+                       break;
+               }
+
+               $template->set( 'challenge-status-html', $out );
+
+               return $template->getHTML();
+       }
+}
\ No newline at end of file
diff --git a/challenge.sql b/challenge.sql
new file mode 100644
index 0000000..a55ed2c
--- /dev/null
+++ b/challenge.sql
@@ -0,0 +1,43 @@
+CREATE TABLE /*_*/challenge (
+  challenge_id int(11) NOT NULL auto_increment PRIMARY KEY,
+  -- Challenger
+  challenge_user_id_1 int(11) NOT NULL default 0,
+  challenge_username1 varchar(255) NOT NULL default '',
+  -- Challenged
+  challenge_user_id_2 int(11) NOT NULL default 0,
+  challenge_username2 varchar(255) NOT NULL default '',
+  challenge_info varchar(200) NOT NULL default '',
+  challenge_event_date varchar(15) NOT NULL default '0000-00-00',
+  challenge_description text,
+  challenge_win_terms varchar(200) NOT NULL default '',
+  challenge_lose_terms varchar(200) NOT NULL default '',
+  challenge_winner_user_id int(11) NOT NULL default 0,
+  challenge_winner_username varchar(255) NOT NULL default '',
+  challenge_status int(11) NOT NULL default 0,
+  -- The following two fields appear to be currently unused but were used in
+  -- the past by Special:ChallengeAction...the question is: should we drop 
these
+  -- or bring them back?
+  challenge_accept_date datetime NOT NULL default '0000-00-00 00:00:00',
+  challenge_complete_date datetime NOT NULL default '0000-00-00 00:00:00',
+  challenge_date varbinary(14) NOT NULL default ''
+)/*$wgDBTableOptions*/;
+
+CREATE TABLE /*_*/challenge_rate (
+  challenge_rate_id int(11) NOT NULL auto_increment PRIMARY KEY,
+  challenge_id int(11) NOT NULL default 0,
+  challenge_rate_date varbinary(14) NOT NULL default '',
+  challenge_rate_user_id int(11) NOT NULL default 0,
+  challenge_rate_username varchar(255) NOT NULL default '',
+  challenge_rate_submitter_user_id int(11) NOT NULL default 0,
+  challenge_rate_submitter_username varchar(255) NOT NULL default '',
+  challenge_rate_score int(11) NOT NULL default 0,
+  challenge_rate_comment text NOT NULL
+)/*$wgDBTableOptions*/;
+
+CREATE TABLE /*_*/challenge_user_record (
+  challenge_record_user_id int(11) NOT NULL default 0,
+  challenge_record_username varchar(255) NOT NULL default '',
+  challenge_wins int(11) NOT NULL default 0,
+  challenge_losses int(11) NOT NULL default 0,
+  challenge_ties int(11) NOT NULL default 0
+)/*$wgDBTableOptions*/;
\ No newline at end of file
diff --git a/i18n/en.json b/i18n/en.json
new file mode 100644
index 0000000..c59a1a1
--- /dev/null
+++ b/i18n/en.json
@@ -0,0 +1,157 @@
+{
+       "@metadata": {
+               "authors": [
+                       "Aaron Wright",
+                       "David Pean"
+               ]
+       },
+       "challenge-status-rejected": "rejected",
+       "challenge-status-removed": "removed",
+       "challenge-status-awaiting": "awaiting acceptance",
+       "challenge-status-in-progress": "in progress",
+       "challenge-status-completed": "completed",
+       "challenge-js-event-required": "The event is required",
+       "challenge-js-date-required": "Event date is required",
+       "challenge-js-description-required": "Description is required",
+       "challenge-js-win-terms-required": "Win terms are required",
+       "challenge-js-lose-terms-required": "Lose terms required",
+       "challenge-js-challenge-removed": "Challenge has been removed",
+       "challenge-js-accepted": "Accepted",
+       "challenge-js-rejected": "Rejected",
+       "challenge-js-countered": "Countered",
+       "challenge-js-winner-recorded": "Challenge winner has been recorded",
+       "challenge-js-rating-submitted": "Your rating has been submitted",
+       "challenge-js-error-date-format": "The date format should be: 
mm/dd/yyyy",
+       "challenge-js-error-invalid-month": "Please enter a valid month",
+       "challenge-js-error-invalid-day": "Please enter a valid day",
+       "challenge-js-error-invalid-year": "Please enter a valid 4 digit year 
between $1 and $2",
+       "challenge-js-error-invalid-date": "Please enter a valid date",
+       "challenge-js-error-future-date": "Date entered is a future date.",
+       "challenge-js-error-is-backwards": "Begin date is greater than end 
date.",
+       "challenge_request_subject": "$1 has challenged you on {{SITENAME}}!",
+       "challenge_request_body": "Hi $1.\n\n\t$2 has challenged you on 
{{SITENAME}}!\n\nGo to $3 to view the challenge details and respond to 
$2.\n\nThanks\n\nThe {{SITENAME}} Team\n\n---\n\nHey, want to stop getting 
e-mails from us?\n\nClick $4\nand change your settings to disable e-mail 
notifications.",
+       "challenge_accept_subject": "$1 has accepted your challenge on 
{{SITENAME}}!",
+       "challenge_accept_body": "Hi $1.\n\n\t$2 has accepted your challenge on 
{{SITENAME}}!\n\nGo to $3 to view the challenge details.\n\nThanks\n\nThe 
{{SITENAME}} Team\n\n---\n\nHey, want to stop getting e-mails from us?\n\nClick 
$4\nand change your settings to disable e-mail notifications.",
+       "challenge_lose_subject": "Oh no! $1 has beaten you on {{SITENAME}} 
challenge #$2!",
+       "challenge_lose_body": "Hi $1.\n\n\t$2 has won the challenge on 
{{SITENAME}}!\n\nGo to $3 to view the challenge details and 
terms.\n\nThanks\n\nThe {{SITENAME}} Team\n\n---\n\nHey, want to stop getting 
e-mails from us?\n\nClick $4\nand change your settings to disable e-mail 
notifications.",
+       "challenge_win_subject": "Congratulations! You've beaten $1 in 
{{SITENAME}} challenge #$2!",
+       "challenge_win_body": "Hi $1.\n\n\tYou've won the {{SITENAME}} 
challenge against $2!\n\nGo to $3 to view the challenge details and 
terms.\n\nThanks\n\nThe {{SITENAME}} Team\n\n---\n\nHey, want to stop getting 
e-mails from us?\n\nClick $4\nand change your settings to disable e-mail 
notifications.",      "challengeaction": "Challenge Standings",
+       "challengehistory": "Challenge History",
+       "challengehistory-all": "All",
+       "challengehistory-accepted": "accepted",
+       "challengehistory-awaiting": "Awaiting Acceptance",
+       "challengehistory-challenge-user": "Challenge $1",
+       "challengehistory-challenge-someone": "Challenge someone",
+       "challengehistory-challenger": "challenger",
+       "challengehistory-challenger-desc": "challenger description",
+       "challengehistory-completed": "Completed",
+       "challengehistory-completed2": "completed",
+       "challengehistory-empty": "There is no current challenge history.",
+       "challengehistory-event": "event",
+       "challengehistory-filter": "filter:",
+       "challengehistory-inprogress": "In progress",
+       "challengehistory-losers-rating": "loser's rating",
+       "challengehistory-negative": "Negative",
+       "challengehistory-negative2": "$1 negative",
+       "challengehistory-neutral": "Neutral",
+       "challengehistory-neutral2": "$1 neutral",
+       "challengehistory-next": "next",
+       "challengehistory-overall": "Overall Record",
+       "challengehistory-positive": "Positive",
+       "challengehistory-positive2": "$1 positive",
+       "challengehistory-prev": "prev",
+       "challengehistory-ratings-loser": "Ratings When Loser",
+       "challengehistory-recentchallenges": "Recent {{SITENAME}} Challenges",
+       "challengehistory-rejected": "Rejected",
+       "challengehistory-removed": "removed",
+       "challengehistory-status": "status",
+       "challengehistory-target": "target",
+       "challengehistory-user": "Challenge This User",
+       "challengehistory-users-history": "$1's challenge history",
+       "challengehistory-view-standings": "View standings",
+       "challengestandings": "Challenge Standings",
+       "challengestandings-challengeuser": "challenge user",
+       "challengestandings-l": "L",
+       "challengestandings-t": "T",
+       "challengestandings-title": "{{SITENAME}} Challenge Standings",
+       "challengestandings-user": "user",
+       "challengestandings-w": "W",
+       "challengeuser": "Challenge a User",
+       "challengeuser-challenge-sent-title": "You have issued a challenge to 
$1",
+       "challengeuser-completehistory": "View Complete Challenge History",
+       "challengeuser-date": "the event date (mm/dd/yyyy)",
+       "challengeuser-description": "description (ex: I'm taking the Eagles w/ 
the spread (+3))",
+       "challengeuser-enter-info": "Enter challenge information",
+       "challengeuser-error-page-title": "Woops!",
+       "challengeuser-event": "the event (ex: Giants vs. Eagles)",
+       "challengeuser-feedback": "feedback score: <b>$1</b>",
+       "challengeuser-helppage": "Community Challenges",
+       "challengeuser-info": "Challenge Info",
+       "challengeuser-info-body-no-user": "Challenges are a fun way to put 
your wiki where your mouth is!",
+       "challengeuser-info-body": "Challenges are a great way to prove your 
sports knowledge to the community, as well as get others to build your 
content.",
+       "challengeuser-info-title": "What are Challenges?",
+       "challengeuser-info-title-no-user": "Who would you like to challenge?",
+       "challengeuser-login": "You must be logged in to issue challenges.",
+       "challengeuser-loseterms": "lose terms (ex: I am willing to edit the 
2005 team results page)",
+       "challengeuser-nouser": "No user selected. Please challenge a user 
through the correct link.",
+       "challengeuser-or": "or",
+       "challengeuser-record": "record:",
+       "challengeuser-rules": "Please read rules and stuff",
+       "challengeuser-select-friend": "select a friend",
+       "challengeuser-select-friend-from-list": "Select from your list of 
friends",
+       "challengeuser-self": "You cannot challenge yourself!",
+       "challengeuser-sent": "The challenge has been sent, and is awaiting 
acceptance by $1",
+       "challengeuser-start-button": "start challenge",
+       "challengeuser-submit-button": "Submit",
+       "challengeuser-title": "Challenge User",
+       "challengeuser-title-user": "Challenge user $1",
+       "challengeuser-type-username": "If you know the name of the user, type 
it in below",
+       "challengeuser-users-stats": "$1's challenge stats",
+       "challengeuser-view-all-challenges": "View all challenges",
+       "challengeuser-view-userpage": "View $1's userpage",
+       "challengeuser-viewhistory": "You can view your challenge history here",
+       "challengeuser-viewstatus": "You can view the status of your challenge 
here",
+       "challengeuser-winterms": "win terms (ex: My opponent must fill out the 
1991 roster page)",
+       "challengeview": "View challenge",
+       "challengeview-accept": "Accept",
+       "challengeview-accepted": "Accepted",
+       "challengeview-acceptance": "Awaiting acceptance",
+       "challengeview-additionalcomments": "Additional comments (ex: He did a 
lousy job completing the task)",
+       "challengeview-admin": "Admin cancel challenge due to abuse",
+       "challengeview-admintext": "You are an admin, so you can pick the 
winner if the Event has been completed<br />Who won the bet?",
+       "challengeview-by": "by <b>$1</b>",
+       "challengeview-by-on": "by <b>$1</b> on $2",
+       "challengeview-comment": "<b>comment</b>: $1",
+       "challengeview-counterterms": "Counter Terms",
+       "challengeview-description": "$1's description:",
+       "challengeview-event": "Event:",
+       "challengeview-ifwins": "if $1 wins, $2 has to . . . ",
+       "challengeview-inprogress": "In progress -- awaiting completion of 
event and admin approval",
+       "challengeview-invalidid": "Invalid challenge ID",
+       "challengeview-issue-challenge": "issue challenge",
+       "challengeview-negative": "negative",
+       "challengeview-negative2": "Negative",
+       "challengeview-neutral": "neutral",
+       "challengeview-neutral2": "Neutral",
+       "challengeview-nochallenge": "No challenge specified",
+       "challengeview-notyetrated": "This challenge has not yet been rated by 
the winner",
+       "challengeview-positive": "positive",
+       "challengeview-positive2": "Positive",
+       "challengeview-push": "push",
+       "challengeview-rateloser": "Please rate the loser's end of the bargain",
+       "challengeview-rating": "Challenge Rating",
+       "challengeview-rating2": "<b>rating:</b>",
+       "challengeview-reject": "Reject",
+       "challengeview-rejected": "Rejected",
+       "challengeview-removed": "Removed due to violation of rules",
+       "challengeview-sent-to-you": "This challenge has been sent to you. 
Please choose your response",
+       "challengeview-submit-button": "Submit",
+       "challengeview-status": "Challenge Status",
+       "challengeview-title": "{{SITENAME}} Challenge Info",
+       "challengeview-versus": "vs.",
+       "challengeview-view-history": "view challenge history",
+       "challengeview-won-by": "Challenge won by <b>$1</b>",
+       "challengeview-was-push": "Challenge was a push!",
+       "challengeview-you-won": "You won the challenge!",
+       "right-challengeadmin": "Administrate challenges"
+}
diff --git a/i18n/fi.json b/i18n/fi.json
new file mode 100644
index 0000000..48f3d66
--- /dev/null
+++ b/i18n/fi.json
@@ -0,0 +1,149 @@
+{
+       "@metadata": {
+               "authors": [
+                       "Jack Phoenix"
+               ]
+       },
+       "challenge-status-rejected": "hylätty",
+       "challenge-status-removed": "poistettu",
+       "challenge-status-awaiting": "odottaa hyväksyntää",
+       "challenge-status-in-progress": "meneillään",
+       "challenge-status-completed": "valmis",
+       "challenge-js-event-required": "Tapahtuma on pakollinen tieto",
+       "challenge-js-date-required": "Tapahtumapäiväys on pakollinen tieto",
+       "challenge-js-description-required": "Kuvaus on pakollinen tieto",
+       "challenge-js-win-terms-required": "Voiton ehdot ovat pakollisia",
+       "challenge-js-lose-terms-required": "Häviön ehdot ovat pakollisia",
+       "challenge-js-challenge-removed": "Haaste on poistettu",
+       "challenge-js-accepted": "Hyväksytty",
+       "challenge-js-rejected": "Hylätty",
+       "challenge-js-countered": "Vastaehdot annettu",
+       "challenge-js-winner-recorded": "Haasteen voittaja on tallennettu",
+       "challenge-js-rating-submitted": "Arviosi on lähetetty",
+       "challenge-js-error-date-format": "Päiväyksen tulisi olla muodossa 
KK/PP/VVVV",
+       "challenge-js-error-invalid-month": "Anna kelvollinen kuukausi",
+       "challenge-js-error-invalid-day": "Anna kelvollinen päivä",
+       "challenge-js-error-invalid-year": "Anna kelvollinen nelinumeroinen 
vuosi väliltä $1-$2",
+       "challenge-js-error-invalid-date": "Anna kelvollinen päiväys",
+       "challenge-js-error-future-date": "Annettu päiväys on tulevaisuudessa.",
+       "challenge-js-error-is-backwards": "Aloituspäiväys on suurempi kuin 
lopetuspäiväys.",
+       "challengeaction": "Haastetilastot",
+       "challengehistory": "Haastehistoria",
+       "challengehistory-all": "Kaikki",
+       "challengehistory-accepted": "hyväksytty",
+       "challengehistory-awaiting": "Hyväksyntää odottavat",
+       "challengehistory-challenge-user": "Haasta $1",
+       "challengehistory-challenge-someone": "Haasta joku",
+       "challengehistory-challenger": "haastaja",
+       "challengehistory-challenger-desc": "haastajan kuvaus",
+       "challengehistory-completed": "Suoritettu",
+       "challengehistory-empty": "Nykyinen haastehistoria on tyhjä.",
+       "challengehistory-event": "tapahtuma",
+       "challengehistory-filter": "suodata:",
+       "challengehistory-inprogress": "Käynnissä",
+       "challengehistory-inprogress2": "käynnissä",
+       "challengehistory-losers-rating": "häviäjän arvostelu",
+       "challengehistory-negative": "Negatiivinen",
+       "challengehistory-negative2": "{{PLURAL:$1|$1 negatiivinen|$1 
negatiivista}}",
+       "challengehistory-neutral": "Neutraali",
+       "challengehistory-neutral2": "{{PLURAL:$1|$1 neutraali|$1 neutraalia}}",
+       "challengehistory-next": "seur.",
+       "challengehistory-overall": "Yleiskatsaus tuloksista",
+       "challengehistory-positive": "Positiivinen",
+       "challengehistory-positive2": "{{PLURAL:$1|$1 positiivinen|$1 
positiivista}}",
+       "challengehistory-prev": "edell.",
+       "challengehistory-ratings-loser": "Arvostelut kun häviäjä",
+       "challengehistory-recentchallenges": "Tuoreet 
{{GRAMMAR:genitive|{{SITENAME}}}} haasteet",
+       "challengehistory-rejected": "Hylätty",
+       "challengehistory-removed": "poistettu",
+       "challengehistory-status": "tila",
+       "challengehistory-target": "kohde",
+       "challengehistory-user": "Haasta tämä käyttäjä",
+       "challengehistory-users-history": "Käyttäjän $1 haastehistoria",
+       "challengehistory-view-standings": "Tarkastele tilastoja",
+       "challengestandings": "Haastetilastot",
+       "challengestandings-challengeuser": "haasta käyttäjä",
+       "challengestandings-l": "H",
+       "challengestandings-t": "T",
+       "challengestandings-title": "{{GRAMMAR:genitive|{{SITENAME}}}} 
haastetilastot",
+       "challengestandings-user": "käyttäjä",
+       "challengestandings-w": "V",
+       "challengeuser": "Haasta käyttäjä",
+       "challengeuser-challenge-sent-title": "Olet antanut haasteen 
käyttäjälle $1",
+       "challengeuser-completehistory": "Katso täydellinen haastehistoria",
+       "challengeuser-date": "tapahtumapäivä (KK/PP/VVVV)",
+       "challengeuser-description": "kuvaus (esim. I'm taking the Eagles w/ 
the spread (+3))",
+       "challengeuser-enter-info": "Anna haasteen tiedot",
+       "challengeuser-error-page-title": "Ups!",
+       "challengeuser-event": "tapahtuma (esim. Giants vs. Eagles)",
+       "challengeuser-feedback": "palautepisteet: <b>$1</b>",
+       "challengeuser-helppage": "Yhteisöhaasteet",
+       "challengeuser-info": "Haasteen tiedot",
+       "challengeuser-info-body-no-user": "Haasteet ovat hauska tapa laittaa 
wikisi sinne, missä suusi on!",
+       "challengeuser-info-body": "Haasteet ovat loistava tapa todistaa 
yhteisölle urheilutietoutesi sekä saada muut rakentamaan sisältöäsi.",
+       "challengeuser-info-title": "Mitä haasteet ovat?",
+       "challengeuser-info-title-no-user": "Kenet haluaisit haastaa?",
+       "challengeuser-login": "Sinun tulee olla kirjautunut sisään antaaksesi 
haasteita.",
+       "challengeuser-loseterms": "häviön ehdot (esim. suostun muokkaamaan 
vuoden 2005 tulossivua)",
+       "challengeuser-nouser": "Käyttäjää ei valittu. Ole hyvä ja haasta 
käyttäjiä oikean linkin kautta.",
+       "challengeuser-or": "tai",
+       "challengeuser-record": "ennätys:",
+       "challengeuser-rules": "Ole hyvä ja lue säännöt ja muut",
+       "challengeuser-select-friend": "valitse ystävä",
+       "challengeuser-select-friend-from-list": "Valitse ystävälistastasi",
+       "challengeuser-self": "Et voi haastaa itseäsi!",
+       "challengeuser-sent": "Haasteesi on lähetetty ja se odottaa käyttäjän 
$1 hyväksyntää",
+       "challengeuser-start-button": "aloita haaste",
+       "challengeuser-submit-button": "Lähetä",
+       "challengeuser-title": "Haasta käyttäjä",
+       "challengeuser-title-user": "Haasta käyttäjä $1",
+       "challengeuser-type-username": "Jos tiedät käyttäjän nimen, kirjoita se 
alapuolelle",
+       "challengeuser-users-stats": "Käyttäjän $1 haastetilastot",
+       "challengeuser-view-all-challenges": "Tarkastele kaikkia haasteita",
+       "challengeuser-view-userpage": "Tarkastele käyttäjän $1 käyttäjäsivua",
+       "challengeuser-viewhistory": "Voit katsoa haastehistoriasi täällä",
+       "challengeuser-viewstatus": "Voit katsoa haasteesi tilan täällä",
+       "challengeuser-winterms": "voiton ehdot (esim. vastustajani tulee 
täyttää vuoden 1991 sivu)",
+       "challengeview": "Haastenäkymä",
+       "challengeview-accept": "Hyväksy",
+       "challengeview-accepted": "Hyväksytty",
+       "challengeview-acceptance": "Odotetaan hyväksyntää",
+       "challengeview-additionalcomments": "Lisäkommentit (esim. hän teki 
surkeaa työtä tehtävän suorittamisessa)",
+       "challengeview-admin": "Ylläpitäjän peruutuspainike väärinkäytön 
vuoksi",
+       "challengeview-admintext": "Olet ylläpitäjä, joten voit valita 
voittajan jos tapahtuma on suoritettu<br />Kumpi voitti vedon?",
+       "challengeview-by": "<b>$1</b>",
+       "challengeview-by-on": "<b>$1</b> $2",
+       "challengeview-comment": "<b>kommentti</b>: $1",
+       "challengeview-counterterms": "Vastaehdot",
+       "challengeview-description": "$1:n kuvaus:",
+       "challengeview-event": "Tapahtuma:",
+       "challengeview-ifwins": "jos $1 voittaa, käyttäjän $2 tulee . . . ",
+       "challengeview-inprogress": "Käynnissä -- odotetaan tapahtuman 
suoritusta ja ylläpitäjän hyväksyntää",
+       "challengeview-invalidid": "Kelpaamaton haasteen tunniste",
+       "challengeview-issue-challenge": "haasta",
+       "challengeview-negative": "negatiivinen",
+       "challengeview-negative2": "Negatiivinen",
+       "challengeview-neutral": "neutraali",
+       "challengeview-neutral2": "Neutraali",
+       "challengeview-nochallenge": "Haastetta ei määritetty",
+       "challengeview-notyetrated": "Voittaja ei ole vielä arvostellut tätä 
haastetta",
+       "challengeview-positive": "positiivinen",
+       "challengeview-positive2": "Positiivinen",
+       "challengeview-push": "tasapeli",
+       "challengeview-rateloser": "Ole hyvä ja arvostele häviäjän kauppaosa",
+       "challengeview-rating": "Arvio",
+       "challengeview-rating2": "<b>arvio:</b>",
+       "challengeview-reject": "Hylkää",
+       "challengeview-rejected": "Hylätty",
+       "challengeview-removed": "Poistettu sääntörikkomuksen vuoksi",
+       "challengeview-sent-to-you": "Tämä haaste on lähetetty sinulle. Ole 
hyvä ja valitse vastauksesi",
+       "challengeview-status": "Haasteen tila",
+       "challengeview-submit-button": "Lähetä",
+       "challengeview-title": "{{GRAMMAR:genitive|{{SITENAME}}}} haastetiedot",
+       "challengeview-versus": "vs.",
+       "challengeview-view-history": "tarkastele haastehistoriaa",
+       "challengeview-won-by": "Haasteen voitti <b>$1</b>",
+       "challengeview-was-push": "Haaste oli tasapeli!",
+       "challengeview-you-won": "Voitit haasteen!",
+       "right-challengeadmin": "Hallinnoida haasteita"
+}
diff --git a/resources/css/ext.challenge.history.css 
b/resources/css/ext.challenge.history.css
new file mode 100644
index 0000000..8a70d49
--- /dev/null
+++ b/resources/css/ext.challenge.history.css
@@ -0,0 +1,54 @@
+select[name="status-filter"] {
+       font-size: 10px;
+}
+
+.challenge-nav {
+       padding-bottom: 25px;
+       width: 740;
+}
+
+.challenge-history-table {
+       border-collapse: collapse; /* cellspacing=0 equivalent */
+       border: 0;
+       padding: 3px;
+       width: 830px;
+}
+
+.challenge-history-header {
+       color: #666666;
+       font-size: 12px;
+       font-weight: 800;
+       padding-right: 5px;
+}
+
+.challenge-history-empty {
+       font-size: 11px;
+}
+
+.challenge-data {
+       border-bottom: 1px solid #eeeeee;
+       font-size: 11px;
+       padding: 5px;
+       vertical-align: top;
+}
+
+.challenge-data img {
+       padding: 2px;
+}
+
+.challenge-data-description {
+       width: 150px;
+}
+
+.challenge-history-filter {
+       float: right;
+}
+
+.challenge-link {
+       float: left;
+}
+
+.challenge-link a {
+       font-weight: 800;
+       font-size: 15px;
+}
\ No newline at end of file
diff --git a/resources/css/ext.challenge.standings.css 
b/resources/css/ext.challenge.standings.css
new file mode 100644
index 0000000..fa88f7a
--- /dev/null
+++ b/resources/css/ext.challenge.standings.css
@@ -0,0 +1,31 @@
+.challenge-standings-table {
+       border: 0;
+       border-collapse: collapse;
+       padding: 3px;
+}
+
+.challenge-standings-title {
+       font-size: 12px;
+       font-weight: 800;
+       padding-right: 5px;
+}
+
+.challenge-standings {
+       border-bottom: 1px solid #eeeeee;
+       font-size: 11px;
+       padding: 5px;
+       vertical-align: top;
+}
+
+.challenge-standings img {
+       padding: 2px;
+}
+
+.challenge-standings-user-link {
+       color: #666;
+}
+
+.challenge-standings-history-link {
+       font-size: 13px;
+       font-weight: bold;
+}
\ No newline at end of file
diff --git a/resources/css/ext.challenge.user.css 
b/resources/css/ext.challenge.user.css
new file mode 100644
index 0000000..1e62a87
--- /dev/null
+++ b/resources/css/ext.challenge.user.css
@@ -0,0 +1,49 @@
+.challenge-user-top {
+       padding-bottom: 15px;
+}
+
+.challenge-user-title {
+       color: #78BA5D;
+       font-size: 15px;
+       font-weight: 800;
+       padding-bottom: 4px;
+}
+
+.challenge-field {
+       padding-bottom: 10px;
+}
+
+.challenge-label {
+       color: #666666;
+}
+
+.challenge-user-info {
+       padding-bottom: 20px;
+       width: 400px;
+}
+
+.challenge-info {
+       border: 1px solid #cccccc;
+       float: right;
+       padding: 5px;
+       width: 200px;
+}
+
+.challenge-user-top a {
+       font-size: 12px;
+       font-weight: 800;
+       text-decoration: none;
+}
+
+.challenge-user-avatar {
+       float: left;
+       width: 75px;
+}
+
+.challenge-user-stats {
+       float: right;
+}
+
+.challenge-user-or {
+       margin: 10px 0px 10px 0px;
+}
\ No newline at end of file
diff --git a/resources/css/ext.challenge.view.css 
b/resources/css/ext.challenge.view.css
new file mode 100644
index 0000000..fd0abbf
--- /dev/null
+++ b/resources/css/ext.challenge.view.css
@@ -0,0 +1,55 @@
+.challenge-main-table {
+       background-color: #eeeeee;
+       border: 1px solid #cccccc;
+       border-collapse: collapse; /* cellspacing=0 equivalent */
+       padding: 8px;
+}
+
+.challenge-title {
+       color: #78BA5D;
+       font-size: 15px;
+       font-weight: 800;
+       padding-bottom: 10px;
+}
+
+.challenge-small-link {
+       font-size: 11px;
+}
+
+.challenge-user-link {
+       font-size: 14px;
+       font-weight: 800;
+}
+
+.challenge-status-text {
+       color: #666666;
+       font-size: 14px;
+       font-weight: 800;
+}
+
+.challenge-form {
+       font-size: 12px;
+       font-weight: 400;
+}
+
+/* Admin-only link for cancelling the challenge */
+a.challenge-admin-cancel-link {
+       color: #990000;
+}
+
+.challenge-terms-container {
+       border-collapse: collapse; /* cellspacing=0 equivalent */
+       padding: 0;
+}
+
+.challenge-terms {
+       border-collapse: collapse; /* cellspacing=0 equivalent */
+       padding: 0;
+       width: 300px;
+}
+
+.challenge-line {
+       border-bottom: 1px solid #cccccc;
+       margin-bottom: 15px;
+       width: 800px;
+}
\ No newline at end of file
diff --git a/resources/images/userpageIcon.png 
b/resources/images/userpageIcon.png
new file mode 100644
index 0000000..ae2bb9b
--- /dev/null
+++ b/resources/images/userpageIcon.png
Binary files differ
diff --git a/resources/js/Challenge.js b/resources/js/Challenge.js
new file mode 100644
index 0000000..cbd0aef
--- /dev/null
+++ b/resources/js/Challenge.js
@@ -0,0 +1,153 @@
+var Challenge = {
+       chooseFriend: function( friend ) {
+               window.location = mw.util.getUrl( 'Special:ChallengeUser', { 
'user': friend } );
+       },
+
+       changeFilter: function( user, status ) {
+               window.location = mw.util.getUrl( 'Special:ChallengeHistory', { 
'user': user, 'status': status } );
+       },
+
+       send: function() {
+               var err = '',
+                       req = [
+                               // field name | i18n message name
+                               'info|challenge-js-event-required',
+                               'date|challenge-js-date-required',
+                               'description|challenge-js-description-required',
+                               'win|challenge-js-win-terms-required',
+                               'lose|challenge-js-lose-terms-required'
+                       ];
+
+               for ( var x = 0; x <= req.length - 1; x++ ) {
+                       var fld = req[x].split( '|' );
+                       if ( document.getElementById( fld[0] ) == '' ) {
+                               err += mw.msg( fld[1] ) + '\n';
+                       }
+               }
+
+               if ( !err ) {//&& isDate($F('date'))==true){
+                       document.challenge.submit();
+               } else {
+                       if ( err ) {
+                               alert( err );
+                       }
+               }
+       },
+
+       cancel: function( id ) {
+               $.ajax( {
+                       type: 'POST',
+                       url: mw.util.wikiScript( 'index' ),
+                       data: {
+                               title: 'Special:ChallengeAction',
+                               action: 1,
+                               'id': id,
+                               status: -2
+                       }
+               } ).done( function() {
+                       var text = mw.msg( 'challenge-js-challenge-removed' );
+                       alert( text );
+                       $( '#challenge-status' ).text( text );
+               } );
+       },
+
+       response: function() {
+               $( '#challenge-status' ).hide();
+               $.ajax( {
+                       type: 'POST',
+                       url: mw.util.wikiScript( 'index' ),
+                       data: {
+                               title: 'Special:ChallengeAction',
+                               action: 1,
+                               'id': $( '#challenge_id' ).val(),
+                               status: $( '#challenge_action' ).val()
+                       }
+               } ).done( function() {
+                       var newStatus;
+                       switch ( parseInt( $( '#challenge_action' ).val() ) ) {
+                               case 1:
+                                       newStatus = mw.msg( 
'challenge-js-accepted' );
+                                       break;
+                               case -1:
+                                       newStatus = mw.msg( 
'challenge-js-rejected' );
+                                       break;
+                               case 2:
+                                       newStatus = mw.msg( 
'challenge-js-countered' );
+                                       break;
+                       }
+
+                       $( '#challenge-status' ).text( newStatus ).show( 500 );
+               } );
+       },
+
+       approval: function() {
+               $( '#challenge-status' ).hide();
+               $.ajax( {
+                       type: 'POST',
+                       url: mw.util.wikiScript( 'index' ),
+                       data: {
+                               title: 'Special:ChallengeAction',
+                               action: 2,
+                               'id': $( '#challenge_id' ).val(),
+                               userid: $( '#challenge_winner_userid' ).val()
+                       }
+               } ).done( function() {
+                       $( '#challenge-status' ).text( mw.msg( 
'challenge-js-winner-recorded' ) ).show( 500 );
+               } );
+       },
+
+       rate: function() {
+               $( '#challenge-status' ).hide();
+               $.ajax( {
+                       type: 'POST',
+                       url: mw.util.wikiScript( 'index' ),
+                       data: {
+                               title: 'Special:ChallengeAction',
+                               action: 3,
+                               'id': $( '#challenge_id' ).val(),
+                               challenge_rate: $( '#challenge_rate' ).val(),
+                               rate_comment: $( '#rate_comment' ).val(),
+                               loser_userid: $( '#loser_userid' ).val(),
+                               loser_username: $( '#loser_username' ).val()
+                       }
+               } ).done( function() {
+                       $( '#challenge-status' ).text( mw.msg( 
'challenge-js-rating-submitted' ) ).show( 500 );
+               } );
+       }
+};
+
+$( document ).ready( function() {
+       // Special:ChallengeHistory (SpecialChallengeHistory.php)
+       $( 'select[name="status-filter"]' ).on( 'change', function() {
+               Challenge.changeFilter( $( this ).data( 'username' ), $( this 
).val() );
+       } );
+
+       // Special:ChallengeUser (SpecialChallengeUser.php)
+       $( '#challenge-user-selector' ).on( 'change', function() {
+               Challenge.chooseFriend( this.value );
+       } );
+
+       // Special:ChallengeUser (templates/challengeuser.tmpl.php)
+       $( 'input.challenge-send-button' ).on( 'click', function() {
+               Challenge.send();
+       } );
+
+       // Special:ChallengeView (templates/challengeview.tmpl.php)
+       $( 'a.challenge-admin-cancel-link' ).on( 'click', function( e ) {
+               e.preventDefault();
+               Challenge.cancel( $( this ).data( 'challenge-id' ) );
+       } );
+
+       // Special:ChallengeView (SpecialChallengeView.php)
+       $( 'input.challenge-approval-button' ).on( 'click', function() {
+               Challenge.approval();
+       } );
+
+       $( 'input.challenge-rate-button' ).on( 'click', function() {
+               Challenge.rate();
+       } );
+
+       $( 'input.challenge-response-button' ).on( 'click', function() {
+               Challenge.response();
+       } );
+} );
\ No newline at end of file
diff --git a/resources/js/DatePicker.js b/resources/js/DatePicker.js
new file mode 100644
index 0000000..aab609c
--- /dev/null
+++ b/resources/js/DatePicker.js
@@ -0,0 +1,9 @@
+$( document ).ready( function() {
+       mw.loader.using( 'jquery.ui.datepicker', function() {
+               $( '#date' ).datepicker( {
+                       changeYear: true,
+                       yearRange: '1930:c',
+                       dateFormat: 'mm/dd/yy'
+               } );
+       } );
+} );
\ No newline at end of file
diff --git a/resources/js/ValidateDate.js b/resources/js/ValidateDate.js
new file mode 100644
index 0000000..f045732
--- /dev/null
+++ b/resources/js/ValidateDate.js
@@ -0,0 +1,134 @@
+var ChallengeDateValidator = {
+       // Declaring valid date character, minimum year and maximum year
+       dtCh: '/',
+       minYear: 1900,
+       maxYear: 2100,
+
+       isInteger: function( sx ) {
+               for ( var i = 0; i < sx.length; i++ ) {
+                       // Check that current character is number.
+                       var c = sx.charAt( i );
+                       if ( ( ( c < '0' ) || ( c > '9' ) ) ) {
+                               return false;
+                       }
+               }
+
+               // All characters are numbers.
+               return true;
+       },
+
+       stripCharsInBag: function( sx, bag ) {
+               var returnString = '';
+               // Search through string's characters one by one.
+               // If character is not in bag, append to returnString.
+               for ( var i = 0; i < sx.length; i++ ) {
+                       var c = sx.charAt( i );
+                       if ( bag.indexOf( c ) == -1 ) {
+                               returnString += c;
+                       }
+               }
+               return returnString;
+       },
+
+       daysInFebruary: function( year ) {
+               // February has 29 days in any year evenly divisible by four,
+               // EXCEPT for centurial years which are not also divisible by 
400.
+               return ( ( ( year % 4 == 0 ) && ( ( !( year % 100 == 0 ) ) || ( 
year % 400 == 0 ) ) ) ? 29 : 28 );
+       },
+
+       DaysArray: function( nx ) {
+               for ( var i = 1; i <= nx; i++ ) {
+                       this[i] = 31;
+                       if ( i == 4 || i == 6 || i == 9 || i == 11 ) {
+                               this[i] = 30;
+                       }
+                       if ( i == 2 ) {
+                               this[i] = 29;
+                       }
+               }
+               return this;
+       },
+
+       isDate: function( dtStr ) {
+               var daysInMonth = ChallengeDateValidator.DaysArray( 12 );
+               var pos1 = dtStr.indexOf( ChallengeDateValidator.dtCh );
+               var pos2 = dtStr.indexOf( ChallengeDateValidator.dtCh, pos1 + 1 
);
+               var strMonth = dtStr.substring( 0, pos1 );
+               var strDay = dtStr.substring( pos1 + 1, pos2 );
+               var strYear = dtStr.substring( pos2 + 1 );
+
+               strYr = strYear;
+
+               if ( strDay.charAt( 0 ) == '0' && strDay.length > 1 ) {
+                       strDay = strDay.substring( 1 );
+               }
+               if ( strMonth.charAt( 0 ) == '0' && strMonth.length > 1 ) {
+                       strMonth = strMonth.substring( 1 );
+               }
+
+               for ( var i = 1; i <= 3; i++ ) {
+                       if ( strYr.charAt( 0 ) == '0' && strYr.length > 1 ) {
+                               strYr = strYr.substring( 1 );
+                       }
+               }
+
+               month = parseInt( strMonth );
+               day = parseInt( strDay );
+               year = parseInt( strYr );
+
+               if ( pos1 == -1 || pos2 == -1 ) {
+                       alert( mw.msg( 'challenge-js-error-date-format' ) );
+                       return false;
+               }
+               if ( strMonth.length < 1 || month < 1 || month > 12 ) {
+                       alert( mw.msg( 'challenge-js-error-invalid-month' ) );
+                       return false;
+               }
+               if (
+                       strDay.length < 1 || day < 1 || day > 31 ||
+                       ( month == 2 && day > 
ChallengeDateValidator.daysInFebruary( year ) ) ||
+                       day > ChallengeDateValidator.DaysArray[month]
+               )
+               {
+                       alert( mw.msg( 'challenge-js-error-invalid-day' ) );
+                       return false;
+               }
+               if ( strYear.length != 4 || year == 0 || year < 
ChallengeDateValidator.minYear || year > ChallengeDateValidator.maxYear ) {
+                       alert( mw.msg( 'challenge-js-error-invalid-year', 
ChallengeDateValidator.minYear, ChallengeDateValidator.maxYear ) );
+                       return false;
+               }
+               if (
+                       dtStr.indexOf( ChallengeDateValidator.dtCh, pos2 + 1 ) 
!= -1 ||
+                       ChallengeDateValidator.isInteger( 
ChallengeDateValidator.stripCharsInBag( dtStr, ChallengeDateValidator.dtCh ) ) 
=== false
+               )
+               {
+                       alert( mw.msg( 'challenge-js-error-invalid-date' ) );
+                       return false;
+               }
+               return true;
+       },
+
+       isFuture: function( dtStr ) {
+               var today = new Date();
+               var tstDate = new Date( dtStr );
+
+               if ( Math.round( ( tstDate - today ) / ( 60 * 60 * 60 * 24 ) ) 
< 0 ) {
+                       return true;
+               } else {
+                       alert( mw.msg( 'challenge-js-error-future-date' ) );
+                       return false;
+               }
+       },
+
+       isBackwards: function( dtBeg, dtEnd ) {
+               var startDate = new Date( dtBeg );
+               var endDate = new Date( dtEnd );
+
+               if ( Math.round( ( endDate - startDate ) / ( 60 * 60 * 60 * 24 
) ) >= 0 ) {
+                       return true;
+               } else {
+                       alert( mw.msg( 'challenge-js-error-is-backwards' ) );
+                       return false;
+               }
+       }
+};
\ No newline at end of file
diff --git a/templates/challengeuser.tmpl.php b/templates/challengeuser.tmpl.php
new file mode 100644
index 0000000..727facc
--- /dev/null
+++ b/templates/challengeuser.tmpl.php
@@ -0,0 +1,98 @@
+<?php
+/**
+ * @file
+ */
+if ( !defined( 'MEDIAWIKI' ) ) {
+       die( -1 );
+}
+
+/**
+ * HTML template for Special:ChallengeUser
+ * @ingroup Templates
+ */
+class ChallengeUserTemplate extends QuickTemplate {
+       public function execute() {
+?>
+       <div class="challenge-user-top">
+               <?php echo Linker::link(
+                       $this->data['user_title'],
+                       wfMessage(
+                               'challengeuser-view-userpage',
+                               $this->data['class']->user_name_to
+                       )->escaped()
+               ) . ' - ' .
+               Linker::link(
+                       $this->data['challenge_history_title'],
+                       wfMessage( 'challengeuser-completehistory' )->plain(),
+                       array(),
+                       array( 'user' => $this->data['class']->user_name_to )
+               ) . ' - ' .
+               Linker::link(
+                       $this->data['challenge_history_title'],
+                       wfMessage( 'challengeuser-view-all-challenges' 
)->plain()
+               );
+               ?>
+       </div>
+               <div class="challenge-info">
+                       <div class="challenge-user-title"><?php echo wfMessage( 
'challengeuser-info-title' )->plain() ?></div>
+                       <?php echo wfMessage( 'challengeuser-info-body' 
)->plain() ?>
+               </div>
+               <div class="challenge-user-info">
+                       <div class="challenge-user-avatar">
+                               <?php echo $this->data['avatar']->getAvatarURL( 
array( 'align' => 'middle' ) ); ?>
+                       </div>
+                       <div class="challenge-user-stats">
+                       <div class="challenge-user-title"><?php echo wfMessage( 
'challengeuser-users-stats', $this->data['class']->user_name_to )->escaped(); 
?></div>
+                       <div class="challenge-user-record"><?php echo 
wfMessage( 'challengeuser-record' )->plain() ?> <b><?php echo 
Challenge::getUserChallengeRecord( $this->data['class']->user_id_to ) 
?></b></div>
+                       <div class="challenge-user-feedback"><?php echo 
wfMessage( 'challengeuser-feedback' )->numParams( $this->data['total'] 
)->parse() ?> (<?php echo
+                       $this->data['class']->getLanguage()->pipeList( array(
+                               wfMessage( 'challengehistory-positive2' 
)->numParams( $this->data['pos'] )->parse(),
+                               wfMessage( 'challengehistory-negative2' 
)->numParams( $this->data['neg'] )->parse(),
+                               wfMessage( 'challengehistory-neutral2' 
)->numParams( $this->data['neu'] )->parse()
+                       ) ); ?>)</div>
+               </div>
+       </div>
+       <div class="cleared"></div>
+
+       <div class="challenge-user-title"><?php echo wfMessage( 
'challengeuser-enter-info' )->plain() ?></div>
+       <form action="" method="post" enctype="multipart/form-data" 
name="challenge">
+               <div class="challenge-field">
+                       <div class="challenge-label"><?php echo wfMessage( 
'challengeuser-event' )->plain() ?></div>
+                       <div class="challenge-form">
+                               <input type="text" class="createbox" size="35" 
name="info" id="info" value="" />
+                       </div>
+               </div>
+               <div class="challenge-field">
+                       <div class="challenge-label"><?php echo wfMessage( 
'challengeuser-date' )->plain() ?></div>
+                       <div class="challenge-form">
+                               <input type="text" class="createbox" size="10" 
name="date" id="date" value="" />
+                       </div>
+               </div>
+               <div class="challenge-field">
+                       <div class="challenge-label"><?php echo wfMessage( 
'challengeuser-description' )->plain() ?></div>
+                       <div class="challenge-form">
+                               <input type="text" class="createbox" size="50" 
name="description" id="description" value="" />
+                       </div>
+               </div>
+
+               <div class="challenge-field">
+                       <div class="challenge-label"><?php echo wfMessage( 
'challengeuser-winterms' )->plain() ?></div>
+                       <div class="challenge-form">
+                               <textarea class="createbox" name="win" id="win" 
rows="2" cols="50"></textarea>
+                       </div>
+               </div>
+
+               <div class="challenge-field">
+                       <div class="challenge-label"><?php echo wfMessage( 
'challengeuser-loseterms' )->plain() ?></div>
+                       <div class="challenge-form">
+                       <textarea class="createbox" name="lose" id="lose" 
rows="2" cols="50"></textarea>
+                       </div>
+               </div>
+               <div class="challenge-buttons">
+                       <input type="button" class="createbox 
challenge-send-button site-button" value="<?php echo wfMessage( 
'challengeuser-submit-button' )->plain() ?>" size="20" />
+               </div>
+               <div class="cleared"></div>
+       </form>
+<?php
+       } // execute()
+} // class
\ No newline at end of file
diff --git a/templates/challengeview.tmpl.php b/templates/challengeview.tmpl.php
new file mode 100644
index 0000000..afec0c8
--- /dev/null
+++ b/templates/challengeview.tmpl.php
@@ -0,0 +1,124 @@
+<?php
+/**
+ * @file
+ */
+if ( !defined( 'MEDIAWIKI' ) ) {
+       die( -1 );
+}
+
+/**
+ * HTML template for Special:ChallengeView
+ * @ingroup Templates
+ */
+class ChallengeViewTemplate extends QuickTemplate {
+       public function execute() {
+               $challenge_history_title = SpecialPage::getTitleFor( 
'ChallengeHistory' );
+               $challenge_user_title = SpecialPage::getTitleFor( 
'ChallengeUser' );
+
+               $challenge = $this->data['challenge'];
+               $user = $this->data['user'];
+?>
+       <table class="challenge-main-table">
+               <tr>
+                       <td><?php echo $this->data['avatar1']->getAvatarURL(); 
?></td>
+                       <td>
+                               <span class="challenge-user-title"><?php echo 
Linker::link(
+                                       $this->data['title1'],
+                                       $this->data['title1']->getText(),
+                                       array( 'class' => 'challenge-user-link' 
)
+                               ) ?></span> (<?php echo 
$this->data['c']->getUserChallengeRecord( $challenge['user_id_1'] ) ?>)
+                               <br /><?php echo Linker::link(
+                                       $challenge_history_title,
+                                       wfMessage( 'challengeview-view-history' 
)->plain(),
+                                       array( 'class' => 
'challenge-small-link' ),
+                                       array( 'user' => 
$this->data['title1']->getDBkey() )
+                               ); ?>
+                               <?php if ( $user->getName() !== 
$this->data['title1']->getText() ) { ?>
+                               <br /><?php echo Linker::link(
+                                       $challenge_user_title,
+                                       wfMessage( 
'challengeview-issue-challenge' )->plain(),
+                                       array( 'class' => 
'challenge-small-link' ),
+                                       array( 'user' => 
$this->data['title1']->getDBkey() )
+                               ); ?>
+                               <?php } ?>
+                       </td>
+                       <td>
+                               <b><?php echo wfMessage( 'challengeview-versus' 
)->plain() ?></b>
+                       </td>
+                       <td><?php echo $this->data['avatar2']->getAvatarURL(); 
?></td>
+                       <td>
+                               <span class="challenge-user-link"><?php echo 
Linker::link(
+                                       $this->data['title2'],
+                                       $this->data['title2']->getText(),
+                                       array( 'class' => 'challenge-user-link' 
)
+                               ) ?></span> (<?php echo 
$this->data['c']->getUserChallengeRecord( $challenge['user_id_2'] ) ?>)
+                               <br /><?php echo Linker::link(
+                                       $challenge_history_title,
+                                       wfMessage( 'challengeview-view-history' 
)->plain(),
+                                       array( 'class' => 
'challenge-small-link' ),
+                                       array( 'user' => 
$this->data['title2']->getDBkey() )
+                               ); ?>
+                               <?php if ( $user->getName() !== 
$this->data['title2']->getText() ) { ?>
+                               <br /><?php echo Linker::link(
+                                       $challenge_user_title,
+                                       wfMessage( 
'challengeview-issue-challenge' )->plain(),
+                                       array( 'class' => 
'challenge-small-link' ),
+                                       array( 'user' => 
$this->data['title2']->getDBkey() )
+                               ); ?>
+                               <?php } ?>
+                       </td>
+               </tr>
+       </table>
+       <br />
+       <table>
+               <tr>
+                       <td>
+                               <b><?php echo wfMessage( 'challengeview-event' 
)->plain() ?></b> <span class="challenge-event"><?php echo $challenge['info'] . 
' [' . $challenge['date'] . ']' ?></span>
+                               <br /><b><?php echo wfMessage( 
'challengeview-description', $challenge['user_name_1'] )->parse() ?></b><span 
class="challenge-description"><?php echo $challenge['description'] ?></span>
+                       </td>
+               </tr>
+       </table>
+
+       <!--</td></tr></table><br />-->
+
+       <table class="challenge-terms-container">
+               <tr>
+                       <td valign="top">
+                               <span class="challenge-title"><?php echo 
wfMessage(
+                                       'challengeview-ifwins',
+                                       $challenge['user_name_1'],
+                                       $challenge['user_name_2']
+                               )->parse() ?></span>
+                               <table class="challenge-terms"><tr><td><?php 
echo $challenge['win_terms'] ?></td></tr></table><br />
+                       </td>
+                       <td width="20">&nbsp;</td>
+                       <td valign="top">
+                               <span class="challenge-title"><?php echo 
wfMessage(
+                                       'challengeview-ifwins',
+                                       $challenge['user_name_2'],
+                                       $challenge['user_name_1']
+                               )->parse() ?></span>
+                               <table class="challenge-terms"><tr><td><?php 
echo $challenge['lose_terms'] ?></td></tr></table>
+                       </td>
+               </tr>
+       </table>
+
+<?php
+       if ( $user->isAllowed( 'challengeadmin' ) && $challenge['user_id_2'] != 
$user->getId() && $challenge['user_id_1'] != $user->getId() ) {
+               $adminLink = "<a class=\"challenge-admin-cancel-link\" 
data-challenge-id=\"{$challenge['id']}\" href=\"#\">";
+               $adminLink .= wfMessage( 'challengeview-admin' )->plain();
+               $adminLink .= '</a>';
+               echo $adminLink;
+       }
+?>
+       <div class="challenge-line"></div>
+       <span class="challenge-title"><?php echo wfMessage( 
'challengeview-status' )->plain() ?></span><br />
+       <div class="challenge-status-text">
+               <span id="challenge-status">
+                       <?php echo $this->data['challenge-status-html']; ?>
+               </span>
+       </div>
+       <span id="status2"></span>
+<?php
+       } // execute()
+} // class
\ No newline at end of file

-- 
To view, visit https://gerrit.wikimedia.org/r/164770
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I87ae4147794558be54c570fd0b7f40c5faf1ae03
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/Challenge
Gerrit-Branch: master
Gerrit-Owner: Jack Phoenix <j...@countervandalism.net>
Gerrit-Reviewer: Jack Phoenix <j...@countervandalism.net>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to