r100172 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r100171‎ | r100172 | r100173 >
Date:22:04, 18 October 2011
Author:reedy
Status:old
Tags:
Comment:
Branch Contest extension into 1.18wmf1
Modified paths:
  • /branches/wmf/1.18wmf1/extensions/Contest (added) (history)

Diff [purge]

Index: branches/wmf/1.18wmf1/extensions/Contest/Contest.alias.php
@@ -0,0 +1,26 @@
 2+<?php
 3+
 4+/**
 5+ * Aliases for the special pages of the Contest extension.
 6+ *
 7+ * @since 0.1
 8+ *
 9+ * @file Contest.alias.php
 10+ * @ingroup Contest
 11+ *
 12+ * @licence GNU GPL v3+
 13+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
 14+ */
 15+
 16+$specialPageAliases = array();
 17+
 18+/** English (English) */
 19+$specialPageAliases['en'] = array(
 20+ 'Contest' => array( 'Contest' ),
 21+ 'Contestant' => array( 'Contestant' ),
 22+ 'Contests' => array( 'Contests' ),
 23+ 'ContestSignup' => array( 'ContestSignup' ),
 24+ 'ContestWelcome' => array( 'ContestWelcome' ),
 25+ 'EditContest' => array( 'EditContest' ),
 26+ 'MyContests' => array( 'MyContests', 'ContestSubmission', 'My contests' ),
 27+);
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/Contest.alias.php
___________________________________________________________________
Added: svn:eol-style
128 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/README
@@ -0,0 +1,42 @@
 2+These is the readme file for the Contest extension.
 3+
 4+Extension page on mediawiki.org: https://www.mediawiki.org/wiki/Extension:Contest
 5+Latest version of the readme file: http://svn.wikimedia.org/viewvc/mediawiki/trunk/extensions/Contest/README?view=co
 6+
 7+== About ==
 8+
 9+Contest is a MediaWiki extension.
 10+
 11+=== Feature overview ===
 12+
 13+* Admin interface for managing contests and their challenges.
 14+* Landing and signup pages for each contest.
 15+* Personal contest list and submission interface for each user.
 16+* Summary pages per contest listing contestants, which can be filtered and sorted.
 17+* Judging interface that allows for rating and commenting on each participant.
 18+* All contests, challenges, contestants, comments and votes can be queried and exported via the API.
 19+
 20+== Credits to other projects ==
 21+
 22+=== jQuery ===
 23+
 24+This extension uses code from the jQuery library.
 25+jQuery is dual licensed under the MIT [0] and GPL [1] licenses.
 26+
 27+=== jQuery UI ===
 28+
 29+This extension uses code from the jQuery UI library.
 30+jQuery is dual licensed under the MIT [0] and GPL [1] licenses.
 31+
 32+=== Fancybox ===
 33+
 34+This extension includes code from the FancyBox library. FancyBox is an open-source
 35+jQuery library dual licensed under the MIT [0] and GPL [1] licenses.
 36+
 37+=== jQuery datetimepicker ===
 38+
 39+This extension includes the jQuery timepicker addon by Trent Richardson.
 40+The datetimepicker addon is dual licensed under the MIT [0] and GPL [1] licenses.
 41+
 42+[0] http://www.opensource.org/licenses/mit-license.php
 43+[1] http://www.opensource.org/licenses/gpl-license.php
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/README
___________________________________________________________________
Added: svn:eol-style
144 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/sql/AddContestEmailFields.sql
@@ -0,0 +1,7 @@
 2+ALTER TABLE /*_*/contests
 3+ ADD COLUMN contest_signup_email VARCHAR(255) NOT NULL,
 4+ ADD COLUMN contest_reminder_email VARCHAR(255) NOT NULL;
 5+
 6+UPDATE /*_*/contests SET
 7+ contest_signup_email = 'MediaWiki:',
 8+ contest_reminder_email = 'MediaWiki:';
\ No newline at end of file
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/sql/AddContestEmailFields.sql
___________________________________________________________________
Added: svn:eol-style
19 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/specials/SpecialContestWelcome.php
@@ -0,0 +1,226 @@
 2+<?php
 3+
 4+/**
 5+ * Contest landing page for participants.
 6+ *
 7+ * @since 0.1
 8+ *
 9+ * @file SpecialContestWelcome.php
 10+ * @ingroup Contest
 11+ *
 12+ * @licence GNU GPL v3 or later
 13+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
 14+ */
 15+class SpecialContestWelcome extends SpecialContestPage {
 16+
 17+ /**
 18+ * Constructor.
 19+ *
 20+ * @since 0.1
 21+ */
 22+ public function __construct() {
 23+ parent::__construct( 'ContestWelcome' );
 24+ }
 25+
 26+ /**
 27+ * Main method.
 28+ *
 29+ * @since 0.1
 30+ *
 31+ * @param string $arg
 32+ */
 33+ public function execute( $subPage ) {
 34+ $subPage = str_replace( '_', ' ', $subPage );
 35+
 36+ if ( !parent::execute( $subPage ) ) {
 37+ return;
 38+ }
 39+
 40+ $out = $this->getOutput();
 41+
 42+ $contest = Contest::s()->selectRow( null, array( 'name' => $subPage ) );
 43+
 44+ /**
 45+ * @var $contest Contest
 46+ */
 47+
 48+ if ( $contest === false ) {
 49+ $this->showError( 'contest-welcome-unknown' );
 50+ $out->addHTML( '<br /><br /><br /><br />' );
 51+ $out->returnToMain();
 52+ }
 53+ else if ( ( $contest->getStatus() == Contest::STATUS_FINISHED ) ||
 54+ ( $contest->getStatus() == Contest::STATUS_EXPIRED ) ) {
 55+ $this->showWarning( 'contest-signup-finished' );
 56+ $out->addHTML( '<br /><br /><br /><br />' );
 57+ $out->returnToMain();
 58+ } else if ( $contest->getStatus() == Contest::STATUS_DRAFT ) {
 59+ $this->showWarning( 'contest-signup-draft' );
 60+ $out->addHTML( '<br /><br /><br /><br />' );
 61+ $out->returnToMain();
 62+ }
 63+ else {
 64+ $this->showEnabledPage( $contest );
 65+ }
 66+ }
 67+
 68+ protected function showEnabledPage( Contest $contest ) {
 69+ $out = $this->getOutput();
 70+
 71+ $alreadySignedup = $this->getUser()->isLoggedIn();
 72+
 73+ if ( $alreadySignedup ) {
 74+ // Check if the user is already a contestant in this contest.
 75+ // If he is, reirect to submission page, else show signup form.
 76+ $alreadySignedup = ContestContestant::s()->selectRow(
 77+ 'id',
 78+ array(
 79+ 'contest_id' => $contest->getId(),
 80+ 'user_id' => $this->getUser()->getId()
 81+ )
 82+ ) !== false;
 83+ }
 84+
 85+ if ( $alreadySignedup ) {
 86+ $out->redirect( SpecialPage::getTitleFor( 'MyContests', $contest->getField( 'name' ) )->getLocalURL() );
 87+ }
 88+ else {
 89+ $out->setPageTitle( $contest->getField( 'name' ) );
 90+
 91+ $this->showIntro( $contest );
 92+ $this->showChallenges( $contest );
 93+ $this->showOpportunities( $contest );
 94+ $this->showRules( $contest );
 95+
 96+ $out->addModules( 'contest.special.welcome' );
 97+ }
 98+ }
 99+
 100+ /**
 101+ * Show the intro text for this contest.
 102+ *
 103+ * @since 0.1
 104+ *
 105+ * @param Contest $contest
 106+ */
 107+ protected function showIntro( Contest $contest ) {
 108+ $this->getOutput()->addWikiText( ContestUtils::getArticleContent( $contest->getField( 'intro' ) ) );
 109+ }
 110+
 111+ /**
 112+ * Show a list of the challenges part of this contest.
 113+ *
 114+ * @since 0.1
 115+ *
 116+ * @param Contest $contest
 117+ */
 118+ protected function showChallenges( Contest $contest ) {
 119+ $this->showNoJSFallback( $contest );
 120+
 121+ $this->getOutput()->addHTML( '<div id="contest-challenges"></div><div style="clear:both"></div>' );
 122+
 123+ $this->addContestJS( $contest );
 124+ }
 125+
 126+ /**
 127+ * Output the needed JS data.
 128+ *
 129+ * @since 0.1
 130+ *
 131+ * @param Contest $contest
 132+ */
 133+ protected function addContestJS( Contest $contest ) {
 134+ $challenges = array();
 135+
 136+ foreach ( $contest->getChallenges() as /* ContestChallenge */ $challenge ) {
 137+ $data = $challenge->toArray();
 138+ $data['target'] = $this->getSignupLink( $contest->getField( 'name' ), $challenge->getId() );
 139+ $challenges[] = $data;
 140+ }
 141+
 142+ $this->getOutput()->addScript(
 143+ Skin::makeVariablesScript(
 144+ array(
 145+ 'ContestChallenges' => $challenges,
 146+ 'ContestConfig' => array()
 147+ )
 148+ )
 149+ );
 150+ }
 151+
 152+ /**
 153+ * Output fallback code for people that have JS disabled or have a crappy browser.
 154+ *
 155+ * @since 0.1
 156+ *
 157+ * @param Contest $contest
 158+ */
 159+ protected function showNoJSFallback( Contest $contest ) {
 160+ $out = $this->getOutput();
 161+
 162+ $out->addHTML( '<noscript>' );
 163+ $out->addHTML( '<p class="errorbox">' . htmlspecialchars( wfMsg( 'contest-welcome-js-off' ) ) . '</p>' );
 164+ // TODO?
 165+ $out->addHTML( '</noscript>' );
 166+ }
 167+
 168+ /**
 169+ * Show the opportunities for this contest.
 170+ *
 171+ * @since 0.1
 172+ *
 173+ * @param Contest $contest
 174+ */
 175+ protected function showOpportunities( Contest $contest ) {
 176+ $this->getOutput()->addWikiText( ContestUtils::getArticleContent( $contest->getField( 'opportunities' ) ) );
 177+ }
 178+
 179+ /**
 180+ * Show the rules for this contest.
 181+ *
 182+ * @since 0.1
 183+ *
 184+ * @param Contest $contest
 185+ */
 186+ protected function showRules( Contest $contest ) {
 187+ $this->getOutput()->addHTML( Html::element(
 188+ 'div',
 189+ array(
 190+ 'id' => 'contest-rules',
 191+ 'data-rules' => ContestUtils::getParsedArticleContent( $contest->getField( 'rules_page' ) )
 192+ )
 193+ ) );
 194+ }
 195+
 196+ /**
 197+ * Gets the URL for the signup links.
 198+ * When the user has to login, this will be to the login page,
 199+ * with a retunrto to the signup page.
 200+ *
 201+ * @since 0.1
 202+ *
 203+ * @param string $contestName
 204+ * @param integer|false $challengeId
 205+ *
 206+ * @return string
 207+ */
 208+ protected function getSignupLink( $contestName, $challengeId = false ) {
 209+ if ( $challengeId !== false ) {
 210+ $contestName .= '/' . $challengeId;
 211+ }
 212+
 213+ $signupTitle = SpecialPage::getTitleFor( 'ContestSignup', $contestName );
 214+
 215+ if ( $this->getUser()->isLoggedIn() ) {
 216+ return $signupTitle->getLocalURL();
 217+ }
 218+ else {
 219+ return SpecialPage::getTitleFor( 'Userlogin' )->getLocalURL( array(
 220+ //'type' => 'signup',
 221+ 'returnto' => $signupTitle->getFullText(),
 222+ 'campaign' => 'contests'
 223+ ) );
 224+ }
 225+ }
 226+
 227+}
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/specials/SpecialContestWelcome.php
___________________________________________________________________
Added: svn:eol-style
1228 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/specials/SpecialContestPage.php
@@ -0,0 +1,184 @@
 2+<?php
 3+
 4+/**
 5+ * Base special page for special pages in the Contest extension,
 6+ * taking care of some common stuff and providing compatibility helpers.
 7+ *
 8+ * @since 0.1
 9+ *
 10+ * @file SpecialContestPage.php
 11+ * @ingroup Contest
 12+ *
 13+ * @licence GNU GPL v3 or later
 14+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
 15+ */
 16+abstract class SpecialContestPage extends SpecialPage {
 17+
 18+ public $subPage;
 19+
 20+ /**
 21+ * @see SpecialPage::getDescription
 22+ *
 23+ * @since 0.1
 24+ * @return String
 25+ */
 26+ public function getDescription() {
 27+ return wfMsg( 'special-' . strtolower( $this->getName() ) );
 28+ }
 29+
 30+ /**
 31+ * Sets headers - this should be called from the execute() method of all derived classes!
 32+ *
 33+ * @since 0.1
 34+ */
 35+ public function setHeaders() {
 36+ $out = $this->getOutput();
 37+ $out->setArticleRelated( false );
 38+ $out->setRobotPolicy( 'noindex,nofollow' );
 39+ $out->setPageTitle( $this->getDescription() );
 40+ }
 41+
 42+ /**
 43+ * Main method.
 44+ *
 45+ * @since 0.1
 46+ *
 47+ * @param string $arg
 48+ */
 49+ public function execute( $subPage ) {
 50+ $this->subPage = $subPage;
 51+
 52+ $this->setHeaders();
 53+ $this->outputHeader();
 54+
 55+ // If the user is authorized, display the page, if not, show an error.
 56+ if ( !$this->userCanExecute( $this->getUser() ) ) {
 57+ $this->displayRestrictionError();
 58+ return false;
 59+ }
 60+
 61+ return true;
 62+ }
 63+
 64+ /**
 65+ * Show a message in an error box.
 66+ *
 67+ * @since 0.1
 68+ *
 69+ * @param string $message
 70+ */
 71+ protected function showError( $message ) {
 72+ $this->getOutput()->addHTML(
 73+ '<p class="visualClear errorbox">' . wfMsgExt( $message, 'parseinline' ) . '</p>'
 74+ );
 75+ }
 76+
 77+ /**
 78+ * Show a message in a warning box.
 79+ *
 80+ * @since 0.1
 81+ *
 82+ * @param string $message
 83+ */
 84+ protected function showWarning( $message ) {
 85+ $this->getOutput()->addHTML(
 86+ '<p class="visualClear warningbox">' . wfMsgExt( $message, 'parseinline' ) . '</p>'
 87+ );
 88+ }
 89+
 90+ /**
 91+ * Show a message in a success box.
 92+ *
 93+ * @since 0.1
 94+ *
 95+ * @param string $message
 96+ */
 97+ protected function showSuccess( $message, $subst = '' ) {
 98+ $this->getOutput()->addHTML(
 99+ '<div class="successbox"><strong><p>' . wfMsgExt( $message, array( 'parseinline' ), $subst ) . '</p></strong></div>'
 100+ );
 101+ }
 102+
 103+ /**
 104+ * Get an array of navigation links.
 105+ *
 106+ * @param string $contestName
 107+ * @param User $user
 108+ * @param string|false $exclude
 109+ *
 110+ * @since 0.1
 111+ *
 112+ * @return array
 113+ */
 114+ protected static function getNavigationLinks( $contestName, User $user, $exclude = false ) {
 115+ $pages = array();
 116+
 117+ $pages['contest-nav-contests'] = array( 'Contests' );
 118+
 119+ if ( $user->isAllowed( 'contestjudge' ) ) {
 120+ $pages['contest-nav-contest'] = array( 'Contest', $contestName );
 121+ }
 122+
 123+ if ( $user->isAllowed( 'contestadmin' ) ) {
 124+ $pages['contest-nav-editcontest'] = array( 'EditContest', $contestName );
 125+ }
 126+
 127+ $pages['contest-nav-contestwelcome'] = array( 'ContestWelcome', $contestName );
 128+
 129+ if ( $user->isAllowed( 'contestparticipant' ) ) {
 130+ $pages['contest-nav-contestsignup'] = array( 'ContestSignup', $contestName );
 131+ }
 132+
 133+ $links = array();
 134+
 135+ foreach ( $pages as $message => $page ) {
 136+ $page = (array)$page;
 137+
 138+ if ( $exclude !== false && $page[0] == $exclude ) {
 139+ continue;
 140+ }
 141+
 142+ $subPage = count( $page ) > 1 ? $page[1] : false;
 143+
 144+ $links[] = Html::element(
 145+ 'a',
 146+ array( 'href' => SpecialPage::getTitleFor( $page[0], $subPage )->getLocalURL() ),
 147+ wfMsgExt( $message, 'parseinline', $subPage )
 148+ );
 149+ }
 150+
 151+ return $links;
 152+ }
 153+
 154+ /**
 155+ * Get the navigation links for the specified contest in a pipe-separated list.
 156+ *
 157+ * @since 0.1
 158+ *
 159+ * @param string $contestName
 160+ * @param User $user
 161+ * @param Language $lang
 162+ * @param boolean $exclude
 163+ * @return string
 164+ */
 165+ public static function getNavigation( $contestName, User $user, Language $lang, $exclude = false ) {
 166+ $links = self::getNavigationLinks( $contestName, $user, $exclude );
 167+ return Html::rawElement( 'p', array(), $lang->pipeList( $links ) );
 168+ }
 169+
 170+ /**
 171+ * Display navigation links.
 172+ *
 173+ * @since 0.1
 174+ *
 175+ * @param string|null $subPage
 176+ */
 177+ protected function displayNavigation( $subPage = null ) {
 178+ if ( is_null( $subPage ) ) {
 179+ $subPage = $this->subPage;
 180+ }
 181+
 182+ $this->getOutput()->addHTML( self::getNavigation( $subPage, $this->getUser(), $this->getLang(), $this->getName() ) );
 183+ }
 184+
 185+}
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/specials/SpecialContestPage.php
___________________________________________________________________
Added: svn:eol-style
1186 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/specials/SpecialContest.php
@@ -0,0 +1,156 @@
 2+<?php
 3+
 4+/**
 5+ * Contest interface for judges.
 6+ *
 7+ * @since 0.1
 8+ *
 9+ * @file SpecialContest.php
 10+ * @ingroup Contest
 11+ *
 12+ * @licence GNU GPL v3 or later
 13+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
 14+ */
 15+class SpecialContest extends SpecialContestPage {
 16+
 17+ /**
 18+ * Constructor.
 19+ *
 20+ * @since 0.1
 21+ */
 22+ public function __construct() {
 23+ parent::__construct( 'Contest', 'contestjudge', false );
 24+ }
 25+
 26+ /**
 27+ * Main method.
 28+ *
 29+ * @since 0.1
 30+ *
 31+ * @param string $arg
 32+ */
 33+ public function execute( $subPage ) {
 34+ $subPage = str_replace( '_', ' ', $subPage );
 35+
 36+ $subPage = explode( '/', $subPage, 2 );
 37+ $challengeTitle = count( $subPage ) > 1 ? $subPage[1] : false;
 38+
 39+ $subPage = $subPage[0];
 40+
 41+ if ( !parent::execute( $subPage ) ) {
 42+ return;
 43+ }
 44+
 45+ $out = $this->getOutput();
 46+
 47+ /**
 48+ * @var $contest Contest
 49+ */
 50+ $contest = Contest::s()->selectRow( null, array( 'name' => $subPage ) );
 51+
 52+ if ( $contest === false ) {
 53+ $out->redirect( SpecialPage::getTitleFor( 'Contests' )->getLocalURL() );
 54+ }
 55+ else {
 56+ $out->setPageTitle( wfMsgExt( 'contest-contest-title', 'parseinline', $contest->getField( 'name' ) ) );
 57+ $this->displayNavigation();
 58+ $this->showGeneralInfo( $contest );
 59+ $this->showContestants( $contest, $challengeTitle );
 60+ }
 61+ }
 62+
 63+ /**
 64+ * Display the general contest info.
 65+ *
 66+ * @since 0.1
 67+ *
 68+ * @param Contest $contest
 69+ */
 70+ protected function showGeneralInfo( Contest $contest ) {
 71+ $out = $this->getOutput();
 72+
 73+ $out->addHTML( Html::openElement( 'table', array( 'class' => 'wikitable contest-summary' ) ) );
 74+
 75+ foreach ( $this->getSummaryData( $contest ) as $stat => $value ) {
 76+ $out->addHTML( '<tr>' );
 77+
 78+ $out->addHTML( Html::element(
 79+ 'th',
 80+ array( 'class' => 'contest-summary-name' ),
 81+ wfMsg( 'contest-contest-' . $stat )
 82+ ) );
 83+
 84+ $out->addHTML( Html::element(
 85+ 'td',
 86+ array( 'class' => 'contest-summary-value' ),
 87+ $value
 88+ ) );
 89+
 90+ $out->addHTML( '</tr>' );
 91+ }
 92+
 93+ $out->addHTML( Html::closeElement( 'table' ) );
 94+ }
 95+
 96+ /**
 97+ * Gets the summary data.
 98+ *
 99+ * @since 0.1
 100+ *
 101+ * @param Contest $contest
 102+ *
 103+ * @return array
 104+ */
 105+ protected function getSummaryData( Contest $contest ) {
 106+ $stats = array();
 107+
 108+ $stats['name'] = $contest->getField( 'name' );
 109+ $stats['status'] = Contest::getStatusMessage( $contest->getStatus() );
 110+ $stats['submissioncount'] = $this->getLang()->formatNum( $contest->getField( 'submission_count' ) );
 111+
 112+ return $stats;
 113+ }
 114+
 115+ /**
 116+ * Show a paged list of the contestants foe this contest.
 117+ *
 118+ * @since 0.1
 119+ *
 120+ * @param Contest $contest
 121+ * @param string|false $challengeTitle
 122+ */
 123+ protected function showContestants( Contest $contest, $challengeTitle ) {
 124+ $out = $this->getOutput();
 125+
 126+ $out->addHTML( Html::element( 'h3', array(), wfMsg( 'contest-contest-contestants' ) ) );
 127+
 128+ $conds = array(
 129+ 'contestant_contest_id' => $contest->getId()
 130+ );
 131+
 132+ if ( $challengeTitle !== false ) {
 133+ $challenge = ContestChallenge::s()->selectRow( 'id', array( 'title' => $challengeTitle ) );
 134+
 135+ if ( $challenge !== false ) {
 136+ $conds['contestant_challenge_id'] = $challenge->getField( 'id' );
 137+ unset( $conds['contestant_contest_id'] ); // Not needed because the challenge implies the context
 138+ }
 139+ }
 140+
 141+ $out->addWikiMsg( 'contest-contest-contestants-text' );
 142+
 143+ $pager = new ContestantPager( $this, $conds );
 144+
 145+ if ( $pager->getNumRows() ) {
 146+ $out->addHTML(
 147+ $pager->getNavigationBar() .
 148+ $pager->getBody() .
 149+ $pager->getNavigationBar()
 150+ );
 151+ }
 152+ else {
 153+ $out->addWikiMsg( 'contest-contest-no-results' );
 154+ }
 155+ }
 156+
 157+}
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/specials/SpecialContest.php
___________________________________________________________________
Added: svn:eol-style
1158 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/specials/SpecialContestant.php
@@ -0,0 +1,354 @@
 2+<?php
 3+
 4+/**
 5+ * Contest interface for judges.
 6+ *
 7+ * @since 0.1
 8+ *
 9+ * @file SpecialContest.php
 10+ * @ingroup Contest
 11+ *
 12+ * @licence GNU GPL v3 or later
 13+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
 14+ */
 15+class SpecialContestant extends SpecialContestPage {
 16+
 17+ /**
 18+ * Constructor.
 19+ *
 20+ * @since 0.1
 21+ */
 22+ public function __construct() {
 23+ parent::__construct( 'Contestant', 'contestjudge', false );
 24+ }
 25+
 26+ /**
 27+ * Main method.
 28+ *
 29+ * @since 0.1
 30+ *
 31+ * @param string $arg
 32+ */
 33+ public function execute( $subPage ) {
 34+ if ( !parent::execute( $subPage ) ) {
 35+ return;
 36+ }
 37+
 38+ $contestant = ContestContestant::s()->selectRow( 'id', array( 'id' => (int)$subPage ) );
 39+
 40+ if ( $contestant === false ) {
 41+ $this->getOutput()->redirect( SpecialPage::getTitleFor( 'Contests' )->getLocalURL() );
 42+ }
 43+ else {
 44+ if ( $this->getRequest()->wasPosted()
 45+ && $this->getUser()->matchEditToken( $this->getRequest()->getVal( 'wpEditToken' ) ) )
 46+ {
 47+ $this->handleSubmission( $contestant->getId() );
 48+ }
 49+
 50+ $contestant->loadFields();
 51+ $this->showPage( $contestant );
 52+ }
 53+ }
 54+
 55+ /**
 56+ * Handle a submission by inserting/updating the vote
 57+ * and (optionally) adding the comment.
 58+ *
 59+ * @since 0.1
 60+ *
 61+ * @param integer $contestantId
 62+ *
 63+ * @return boolean Success indicator
 64+ */
 65+ protected function handleSubmission( $contestantId ) {
 66+ $success = true;
 67+
 68+ if ( trim( $this->getRequest()->getText( 'new-comment-text' ) ) !== '' ) {
 69+ $comment = new ContestComment( array(
 70+ 'user_id' => $this->getUser()->getId(),
 71+ 'contestant_id' => $contestantId,
 72+
 73+ 'text' => $this->getRequest()->getText( 'new-comment-text' ),
 74+ 'time' => wfTimestampNow()
 75+ ) );
 76+
 77+ $success = $comment->writeToDB();
 78+
 79+ if ( $success ) {
 80+ ContestContestant::s()->addToField( 'comments', 1 );
 81+ }
 82+ }
 83+
 84+ if ( $success && !is_null( $this->getRequest()->getVal( 'contestant-rating' ) ) ) {
 85+ $attribs = array(
 86+ 'value' => $this->getRequest()->getInt( 'contestant-rating' ),
 87+ 'contestant_id' => $contestantId,
 88+ 'user_id' => $this->getUser()->getId()
 89+ );
 90+
 91+ if ( !is_null( $this->getRequest()->getVal( 'contestant-vote-id' ) ) ) {
 92+ $attribs['id'] = $this->getRequest()->getInt( 'contestant-vote-id' );
 93+ }
 94+
 95+ $vote = new ContestVote( $attribs );
 96+ $success = $vote->writeToDB() && $success;
 97+ }
 98+
 99+ return $success;
 100+ }
 101+
 102+ /**
 103+ * Show the actual page, conisting of the navigation, the summary and
 104+ * the rating and voting controls.
 105+ *
 106+ * @since 0.1
 107+ *
 108+ * @param ContestContestant $contestant
 109+ */
 110+ protected function showPage( ContestContestant $contestant ) {
 111+ global $wgScript;
 112+ $out = $this->getOutput();
 113+
 114+ $out->setPageTitle( wfMsgExt(
 115+ 'contest-contestant-title',
 116+ 'parseinline',
 117+ $contestant->getField( 'id' ),
 118+ $contestant->getContest()->getField( 'name' )
 119+ ) );
 120+
 121+ $this->displayNavigation( str_replace( ' ', '_', $contestant->getContest()->getField( 'name' ) ) );
 122+
 123+ $this->showGeneralInfo( $contestant );
 124+
 125+ $out->addHTML( '<form method="post" action="' . htmlspecialchars( $wgScript ) . '">' );
 126+ $out->addHTML( Html::hidden( 'title', $this->getTitle( $this->subPage )->getPrefixedDBkey() ) );
 127+ $out->addHTML( Html::hidden( 'wpEditToken', $this->getUser()->editToken() ) );
 128+
 129+ $this->showRating( $contestant );
 130+ $this->showComments( $contestant );
 131+
 132+ $out->addHTML( '</form>' );
 133+
 134+ $out->addModules( 'contest.special.contestant' );
 135+ }
 136+
 137+ /**
 138+ * Display the general contestant info.
 139+ *
 140+ * @since 0.1
 141+ *
 142+ * @param ContestContestant $contestant
 143+ */
 144+ protected function showGeneralInfo( ContestContestant $contestant ) {
 145+ $out = $this->getOutput();
 146+
 147+ $out->addHTML( Html::openElement( 'table', array( 'class' => 'wikitable contestant-summary' ) ) );
 148+
 149+ foreach ( $this->getSummaryData( $contestant ) as $stat => $value ) {
 150+ $out->addHTML( '<tr>' );
 151+
 152+ $out->addHTML( Html::element(
 153+ 'th',
 154+ array( 'class' => 'contestant-summary-name' ),
 155+ wfMsg( 'contest-contestant-header-' . $stat )
 156+ ) );
 157+
 158+ $out->addHTML( Html::rawElement(
 159+ 'td',
 160+ array( 'class' => 'contestant-summary-value' ),
 161+ $value
 162+ ) );
 163+
 164+ $out->addHTML( '</tr>' );
 165+ }
 166+
 167+ $out->addHTML( Html::closeElement( 'table' ) );
 168+ }
 169+
 170+ /**
 171+ * Gets the summary data.
 172+ * Values are escaped.
 173+ *
 174+ * @since 0.1
 175+ *
 176+ * @param ContestContestant $contestant
 177+ *
 178+ * @return array
 179+ */
 180+ protected function getSummaryData( ContestContestant $contestant ) {
 181+ $stats = array();
 182+
 183+ $stats['id'] = htmlspecialchars( $contestant->getField( 'id' ) );
 184+ $stats['contest'] = htmlspecialchars( $contestant->getContest()->getField( 'name' ) );
 185+
 186+ $challengeTitles = ContestChallenge::getTitlesForIds( $contestant->getField( 'challenge_id' ) );
 187+ $stats['challenge'] = htmlspecialchars( $challengeTitles[$contestant->getField( 'challenge_id' )] );
 188+
 189+ if ( $contestant->getField( 'submission' ) === '' ) {
 190+ $stats['submission'] = htmlspecialchars( wfMsg( 'contest-contestant-notsubmitted' ) );
 191+ }
 192+ else {
 193+ $stats['submission'] = '<b>' . Html::element(
 194+ 'a',
 195+ array( 'href' => $contestant->getField( 'submission' ) ),
 196+ wfMsg( 'contest-contestant-submission-url' )
 197+ ) . '</b>';
 198+ }
 199+
 200+ $countries = ContestContestant::getCountries();
 201+ $stats['country'] = htmlspecialchars( $countries[$contestant->getField( 'country' )] );
 202+
 203+ $stats['wmf'] = htmlspecialchars( wfMsg( 'contest-contestant-' . ( $contestant->getField( 'wmf' ) ? 'yes' : 'no' ) ) );
 204+ $stats['volunteer'] = htmlspecialchars( wfMsg( 'contest-contestant-' . ( $contestant->getField( 'volunteer' ) ? 'yes' : 'no' ) ) );
 205+
 206+ $stats['rating'] = htmlspecialchars( wfMsgExt(
 207+ 'contest-contestant-rating',
 208+ 'parsemag',
 209+ $this->getLang()->formatNum( $contestant->getField( 'rating' ) ),
 210+ $this->getLang()->formatNum( $contestant->getField( 'rating_count' ) )
 211+ ) );
 212+
 213+ $stats['comments'] = htmlspecialchars( $this->getLang()->formatNum( $contestant->getField( 'comments' ) ) );
 214+
 215+ return $stats;
 216+ }
 217+
 218+ /**
 219+ * Display the current rating the judge gave if any and a control to
 220+ * (re)-rate.
 221+ *
 222+ * @since 0.1
 223+ *
 224+ * @param ContestContestant $contestant
 225+ */
 226+ protected function showRating( ContestContestant $contestant ) {
 227+ $out = $this->getOutput();
 228+
 229+ $out->addHTML( Html::element( 'h2', array(), wfMsg( 'contest-contestant-rate' ) ) );
 230+
 231+ $vote = ContestVote::s()->selectRow(
 232+ array( 'value', 'id' ),
 233+ array( 'user_id' => $this->getUser()->getId(), 'contestant_id' => $contestant->getId() )
 234+ );
 235+
 236+ if ( $vote === false ) {
 237+ $message = wfMsg( 'contest-contestant-not-voted' );
 238+ }
 239+ else {
 240+ $message = wfMsgExt(
 241+ 'contest-contestant-voted',
 242+ 'parsemag',
 243+ $this->getLang()->formatNum( $vote->getField( 'value' ) )
 244+ );
 245+
 246+ $out->addHTML( Html::hidden( 'contestant-vote-id', $vote->getId() ) );
 247+ }
 248+
 249+ $out->addHTML( Html::element( 'p', array(), $message ) );
 250+
 251+ foreach ( ContestSettings::get( 'voteValues' ) as $value ) {
 252+ $attribs = array(
 253+ 'type' => 'radio',
 254+ 'value' => $value,
 255+ 'name' => 'contestant-rating',
 256+ 'id' => 'contestant-rating-' . $value
 257+ );
 258+
 259+ if ( $vote !== false && $value == $vote->getField( 'value' ) ) {
 260+ $attribs['checked'] = 'checked';
 261+ }
 262+
 263+ $out->addHTML(
 264+ Html::element(
 265+ 'input',
 266+ $attribs
 267+ ) .
 268+ Html::element(
 269+ 'label',
 270+ array( 'for' => 'contestant-rating-' . $value ),
 271+ $this->getLang()->formatNum( $value )
 272+ )
 273+ );
 274+ }
 275+
 276+ }
 277+
 278+ /**
 279+ * Show the comments and a control to add additional ones.
 280+ *
 281+ * @since 0.1
 282+ *
 283+ * @param ContestContestant $contestant
 284+ */
 285+ protected function showComments( ContestContestant $contestant ) {
 286+ $out = $this->getOutput();
 287+
 288+ $out->addHTML( Html::element( 'h2', array(), wfMsg( 'contest-contestant-comments' ) ) );
 289+
 290+ $out->addHTML( '<div class="contestant-comments">' );
 291+
 292+ foreach ( $contestant->getComments() as /* ContestComment */ $comment ) {
 293+ $out->addHTML( $this->getCommentHTML( $comment ) );
 294+ }
 295+
 296+ $out->addHTML( '</div>' );
 297+
 298+ $out->addHTML(
 299+ '<div class="contestant-new-comment">
 300+ <textarea cols="40" rows="10" name="new-comment-text"></textarea>
 301+ </div>'
 302+ );
 303+
 304+ $out->addHTML( Html::input( 'submitChanges', wfMsg( 'contest-contestant-submit' ), 'submit' ) );
 305+ }
 306+
 307+ /**
 308+ * Get the HTML for a single comment.
 309+ *
 310+ * @since 0.1
 311+ *
 312+ * @param ContestComment $comment
 313+ *
 314+ * @return string
 315+ */
 316+ protected function getCommentHTML( ContestComment $comment ) {
 317+ $user = User::newFromId( $comment->getField( 'user_id' ) );
 318+
 319+ $htmlId = 'c' . $comment->getId();
 320+
 321+ $html = Html::rawElement(
 322+ 'div',
 323+ array( 'class' => 'contestant-comment-meta' ),
 324+ Html::element(
 325+ 'a',
 326+ array(
 327+ 'href' => $this->getTitle( $this->subPage )->getLocalURL() . "#$htmlId",
 328+ 'title' => wfMsg( 'contest-contestant-permalink' )
 329+ ),
 330+ '#'
 331+ ) .
 332+ wfMsgHtml(
 333+ 'contest-contestant-comment-by',
 334+ Linker::userLink( $comment->getField( 'user_id' ), $user->getName() ) .
 335+ Linker::userToolLinks( $comment->getField( 'user_id' ), $user->getName() )
 336+ ) . '&#160;&#160;&#160;' . htmlspecialchars( $this->getLang()->timeanddate( $comment->getField( 'time' ), true ) )
 337+ );
 338+
 339+ $html .= Html::rawElement(
 340+ 'div',
 341+ array( 'class' => 'contestant-comment-text mw-content-' . $this->getLang()->getDir() . '' ),
 342+ $this->getOutput()->parse( $comment->getField( 'text' ) )
 343+ );
 344+
 345+ return Html::rawElement(
 346+ 'div',
 347+ array(
 348+ 'class' => 'contestant-comment',
 349+ 'id' => $htmlId
 350+ ),
 351+ $html
 352+ );
 353+ }
 354+
 355+}
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/specials/SpecialContestant.php
___________________________________________________________________
Added: svn:eol-style
1356 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/specials/SpecialContests.php
@@ -0,0 +1,265 @@
 2+<?php
 3+
 4+/**
 5+ * List of contests, with admin and judge links depending on user rights.
 6+ *
 7+ * @since 0.1
 8+ *
 9+ * @file SpecialContests.php
 10+ * @ingroup Contest
 11+ *
 12+ * @licence GNU GPL v3 or later
 13+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
 14+ */
 15+class SpecialContests extends SpecialContestPage {
 16+
 17+ /**
 18+ * Constructor.
 19+ *
 20+ * @since 0.1
 21+ */
 22+ public function __construct() {
 23+ parent::__construct( 'Contests' );
 24+ }
 25+
 26+ /**
 27+ * Returns if the user can access the page or not.
 28+ *
 29+ * @return boolean
 30+ */
 31+ protected function userCanAccess() {
 32+ $user = $this->getUser();
 33+ return
 34+ ( $user->isAllowed( 'contestadmin' ) || $user->isAllowed( 'contestjudge' ) )
 35+ && !$user->isBlocked();
 36+ }
 37+
 38+ /**
 39+ * This page is unlisted because the only way to access it is though a contest
 40+ * landing page.
 41+ *
 42+ * @return false|boolean
 43+ */
 44+ public function isListed() {
 45+ return $this->userCanAccess();
 46+ }
 47+
 48+ /**
 49+ * Main method.
 50+ *
 51+ * @since 0.1
 52+ *
 53+ * @param string $arg
 54+ */
 55+ public function execute( $subPage ) {
 56+ $subPage = str_replace( '_', ' ', $subPage );
 57+
 58+ if ( !parent::execute( $subPage ) ) {
 59+ return;
 60+ }
 61+
 62+ $user = $this->getUser();
 63+
 64+ if ( !$this->userCanAccess() ) {
 65+ $this->displayRestrictionError();
 66+ }
 67+
 68+ if ( $user->isAllowed( 'contestadmin' ) ) {
 69+ $this->displayAddNewControl();
 70+ }
 71+
 72+ $this->displayContests();
 73+ }
 74+
 75+ /**
 76+ * Displays the contests.
 77+ *
 78+ * @since 0.1
 79+ */
 80+ protected function displayContests() {
 81+ $contests = Contest::s()->select( array( 'id', 'name', 'status', 'end', 'submission_count' ) );
 82+
 83+ if ( count( $contests ) > 0 ) {
 84+ $this->displayContestsTable( $contests );
 85+ }
 86+
 87+ $this->getOutput()->addModules( 'ext.contest.special.contests' );
 88+ }
 89+
 90+ /**
 91+ * Displays a small form to add a new campaign.
 92+ *
 93+ * @since 0.1
 94+ */
 95+ protected function displayAddNewControl() {
 96+ $out = $this->getOutput();
 97+
 98+ $out->addHTML( Html::openElement(
 99+ 'form',
 100+ array(
 101+ 'method' => 'post',
 102+ 'action' => SpecialPage::getTitleFor( 'EditContest' )->getLocalURL(),
 103+ )
 104+ ) );
 105+
 106+ $out->addHTML( '<fieldset>' );
 107+
 108+ $out->addHTML( '<legend>' . htmlspecialchars( wfMsg( 'contest-special-addnew' ) ) . '</legend>' );
 109+
 110+ $out->addHTML( Html::element( 'p', array(), wfMsg( 'contest-special-namedoc' ) ) );
 111+
 112+ $out->addHTML( Html::element( 'label', array( 'for' => 'newcontest' ), wfMsg( 'contest-special-newname' ) ) );
 113+
 114+ $out->addHTML( '&#160;' . Html::input( 'newcontest' ) . '&#160;' );
 115+
 116+ $out->addHTML( Html::input(
 117+ 'addnewcontest',
 118+ wfMsg( 'contest-special-add' ),
 119+ 'submit'
 120+ ) );
 121+
 122+ $out->addHTML( Html::hidden( 'newEditToken', $this->getUser()->editToken() ) );
 123+
 124+ $out->addHTML( '</fieldset></form>' );
 125+ }
 126+
 127+ /**
 128+ * Displays a list of all contests.
 129+ *
 130+ * @since 0.1
 131+ *
 132+ * @param array $contests
 133+ */
 134+ protected function displayContestsTable( array /* of Contest */ $contests ) {
 135+ $user = $this->getUser();
 136+ $out = $this->getOutput();
 137+
 138+ $out->addHTML( Html::element( 'h2', array( 'class' => 'contests-title' ), wfMsg( 'contest-special-existing' ) ) );
 139+
 140+ $out->addHTML( Xml::openElement(
 141+ 'table',
 142+ array( 'class' => 'wikitable sortable contests-table' )
 143+ ) );
 144+
 145+ $headers = array(
 146+ Html::element( 'th', array(), wfMsg( 'contest-special-name' ) ),
 147+ Html::element( 'th', array(), wfMsg( 'contest-special-status' ) ),
 148+ Html::element( 'th', array(), wfMsg( 'contest-special-submissioncount' ) )
 149+ );
 150+
 151+ $headers[] = Html::element( 'th', array( 'class' => 'unsortable' ) );
 152+
 153+// if ( $user->isAllowed( 'contestadmin' ) ) {
 154+// $headers[] = Html::element( 'th', array( 'class' => 'unsortable' ), wfMsg( 'contest-special-edit' ) );
 155+// $headers[] = Html::element( 'th', array( 'class' => 'unsortable' ), wfMsg( 'contest-special-delete' ) );
 156+// }
 157+
 158+ $out->addHTML( '<thead><tr>' . implode( '', $headers ) . '</tr></thead>' );
 159+
 160+ $out->addHTML( '<tbody>' );
 161+
 162+ foreach ( $contests as $contest ) {
 163+ /**
 164+ * @var $contest Contest
 165+ */
 166+
 167+ $fields = array();
 168+
 169+ if ( $user->isAllowed( 'contestparticipant' ) ) {
 170+ $name = Html::element(
 171+ 'a',
 172+ array(
 173+ 'href' => SpecialPage::getTitleFor( 'ContestWelcome', $contest->getField( 'name' ) )->getLocalURL()
 174+ ),
 175+ $contest->getField( 'name' )
 176+ );
 177+ }
 178+ else {
 179+ $name = $contest->getField( 'name' );
 180+ }
 181+
 182+ $fields[] = Html::rawElement(
 183+ 'td',
 184+ array( 'data-sort-value' => $contest->getField( 'name' ) ),
 185+ $name
 186+ );
 187+
 188+ $fields[] = Html::element(
 189+ 'td',
 190+ array( 'data-sort-value' => $contest->getStatus() ),
 191+ Contest::getStatusMessage( $contest->getStatus() )
 192+ );
 193+
 194+ $fields[] = Html::element(
 195+ 'td',
 196+ array(),
 197+ $this->getLang()->formatNum( $contest->getField( 'submission_count' ) )
 198+ );
 199+
 200+ $links = array();
 201+
 202+ if ( $user->isAllowed( 'contestjudge' ) ) {
 203+ $links[] = Html::element(
 204+ 'a',
 205+ array(
 206+ 'href' => SpecialPage::getTitleFor( 'Contest', $contest->getField( 'name' ) )->getLocalURL()
 207+ ),
 208+ wfMsg( 'contest-nav-contest' )
 209+ );
 210+ }
 211+
 212+ if ( $user->isAllowed( 'contestadmin' ) ) {
 213+ $links[] = Html::element(
 214+ 'a',
 215+ array(
 216+ 'href' => SpecialPage::getTitleFor( 'EditContest', $contest->getField( 'name' ) )->getLocalURL()
 217+ ),
 218+ wfMsg( 'contest-special-edit' )
 219+ );
 220+
 221+ $links[] = Html::element(
 222+ 'a',
 223+ array(
 224+ 'href' => '#',
 225+ 'class' => 'contest-delete',
 226+ 'data-contest-id' => $contest->getId(),
 227+ 'data-contest-token' => $this->getUser()->editToken( 'deletecontest' . $contest->getId() )
 228+ ),
 229+ wfMsg( 'contest-special-delete' )
 230+ );
 231+ }
 232+
 233+ $links[] = Html::element(
 234+ 'a',
 235+ array(
 236+ 'href' => SpecialPage::getTitleFor( 'ContestWelcome', $contest->getField( 'name' ) )->getLocalURL()
 237+ ),
 238+ wfMsg( 'contest-nav-contestwelcome' )
 239+ );
 240+
 241+ if ( $user->isAllowed( 'contestparticipant' ) ) {
 242+ $links[] = Html::element(
 243+ 'a',
 244+ array(
 245+ 'href' => SpecialPage::getTitleFor( 'ContestSignup', $contest->getField( 'name' ) )->getLocalURL()
 246+ ),
 247+ wfMsg( 'contest-nav-contestsignup' )
 248+ );
 249+ }
 250+
 251+ $fields[] = Html::rawElement(
 252+ 'td',
 253+ array(),
 254+ $this->getLang()->pipeList( $links )
 255+ );
 256+
 257+ $out->addHTML( '<tr>' . implode( '', $fields ) . '</tr>' );
 258+ }
 259+
 260+ $out->addHTML( '</tbody>' );
 261+ $out->addHTML( '</table>' );
 262+
 263+ $out->addModules( 'contest.special.contests' );
 264+ }
 265+
 266+}
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/specials/SpecialContests.php
___________________________________________________________________
Added: svn:eol-style
1267 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/specials/SpecialContestSignup.php
@@ -0,0 +1,394 @@
 2+<?php
 3+
 4+/**
 5+ * Contest signup interface for participants.
 6+ *
 7+ * @since 0.1
 8+ *
 9+ * @file SpecialContestSignup.php
 10+ * @ingroup Contest
 11+ *
 12+ * @licence GNU GPL v3 or later
 13+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
 14+ */
 15+class SpecialContestSignup extends SpecialContestPage {
 16+
 17+ /**
 18+ * Constructor.
 19+ *
 20+ * @since 0.1
 21+ */
 22+ public function __construct() {
 23+ parent::__construct( 'ContestSignup' );
 24+ }
 25+
 26+ /**
 27+ * Main method.
 28+ *
 29+ * @since 0.1
 30+ *
 31+ * @param string $arg
 32+ */
 33+ public function execute( $subPage ) {
 34+ $subPage = str_replace( '_', ' ', $subPage );
 35+
 36+ if ( !parent::execute( $subPage ) ) {
 37+ return;
 38+ }
 39+
 40+ if ( $this->getRequest()->wasPosted() && $this->getUser()->matchEditToken( $this->getRequest()->getVal( 'wpEditToken' ) ) ) {
 41+ $this->showSignupForm( Contest::s()->selectRow( null, array( 'id' => $this->getRequest()->getInt( 'wpcontest-id' ) ) ) );
 42+ }
 43+ else {
 44+ $this->showPage( $subPage );
 45+ }
 46+ }
 47+
 48+ /**
 49+ * This page is unlisted because the only way to access it is though a contest
 50+ * landing page.
 51+ *
 52+ * @return false|boolean
 53+ */
 54+ public function isListed() {
 55+ return false;
 56+ }
 57+
 58+ /**
 59+ * Handle form submission.
 60+ *
 61+ * @since 0.1
 62+ *
 63+ * @return true|array
 64+ */
 65+ public function handleSubmission( array $data ) {
 66+ $user = $this->getUser();
 67+
 68+ $user->setEmail( $data['contestant-email'] );
 69+ $user->setRealName( $data['contestant-realname'] );
 70+ $user->saveSettings();
 71+
 72+ $contestant = new ContestContestant( array(
 73+ 'contest_id' => $data['contest-id'],
 74+ 'user_id' => $user->getId(),
 75+ 'challenge_id' => $data['contestant-challengeid'],
 76+
 77+ 'full_name' => $data['contestant-realname'],
 78+ 'user_name' => $user->getName(),
 79+ 'email' => $data['contestant-email'],
 80+
 81+ 'country' => $data['contestant-country'],
 82+ 'volunteer' => $data['contestant-volunteer'],
 83+ 'wmf' => $data['contestant-wmf'],
 84+ ) );
 85+
 86+ return $contestant->writeToDB();
 87+ }
 88+
 89+ /**
 90+ * Show the page.
 91+ *
 92+ * @since 0.1
 93+ *
 94+ * @param string $subPage
 95+ */
 96+ protected function showPage( $subPage ) {
 97+ $out = $this->getOutput();
 98+
 99+ $subPage = explode( '/', $subPage );
 100+ $contestName = $subPage[0];
 101+ $challengeId = count( $subPage ) > 1 ? $subPage[1] : false;
 102+
 103+ $contest = Contest::s()->selectRow( null, array( 'name' => $contestName ) );
 104+
 105+ if ( $contest === false ) {
 106+ $this->showError( 'contest-signup-unknown' );
 107+ $out->addHTML( '<br /><br /><br /><br />' );
 108+ $out->returnToMain();
 109+ return;
 110+ }
 111+ switch ( $contest->getStatus() ) {
 112+ case Contest::STATUS_ACTIVE:
 113+ $this->showEnabledPage( $contest, $challengeId );
 114+ break;
 115+ case Contest::STATUS_DRAFT:
 116+ $this->showWarning( 'contest-signup-draft' );
 117+ $out->addHTML( '<br /><br /><br /><br />' );
 118+ $out->returnToMain();
 119+ break;
 120+ case Contest::STATUS_FINISHED:
 121+ case Contest::STATUS_EXPIRED:
 122+ $this->showWarning( 'contest-signup-finished' );
 123+ $out->addHTML( '<br /><br /><br /><br />' );
 124+ $out->returnToMain();
 125+ break;
 126+ }
 127+ }
 128+
 129+ /**
 130+ * Handle page request when the contest is enabled.
 131+ *
 132+ * @since 0.1
 133+ *
 134+ * @param Contest $contest
 135+ * @param integer|false $challengeId
 136+ */
 137+ protected function showEnabledPage( Contest $contest, $challengeId ) {
 138+ $out = $this->getOutput();
 139+
 140+ // Check if the user is already a contestant in this contest.
 141+ // If he is, reirect to submission page, else show signup form.
 142+ $contestant = ContestContestant::s()->selectRow(
 143+ 'id',
 144+ array(
 145+ 'contest_id' => $contest->getId(),
 146+ 'user_id' => $this->getUser()->getId()
 147+ )
 148+ );
 149+
 150+ if ( $contestant === false ) {
 151+ $out->setPageTitle( $contest->getField( 'name' ) );
 152+ $out->addWikiMsg( 'contest-signup-header', $contest->getField( 'name' ) );
 153+
 154+ $this->showSignupForm( $contest, $challengeId );
 155+ }
 156+ else {
 157+ $out->redirect( SpecialPage::getTitleFor( 'MyContests', $contest->getField( 'name' ) )->getLocalURL() );
 158+ }
 159+ }
 160+
 161+ /**
 162+ * Display the signup form for this contest.
 163+ *
 164+ * @since 0.1
 165+ *
 166+ * @param Contest $contest
 167+ * @param integer|false $challengeId
 168+ */
 169+ protected function showSignupForm( Contest $contest, $challengeId = false ) {
 170+ $form = new HTMLForm( $this->getFormFields( $contest, $challengeId ), $this->getContext() );
 171+
 172+ $form->setSubmitCallback( array( $this, 'handleSubmission' ) );
 173+ $form->setSubmitText( wfMsg( 'contest-signup-submit' ) );
 174+
 175+ if( $form->show() ) {
 176+ $this->showSucess( $contest );
 177+ }
 178+ else {
 179+ $this->getOutput()->addModules( 'contest.special.signup' );
 180+ }
 181+
 182+ $this->getOutput()->addScript(
 183+ Skin::makeVariablesScript(
 184+ array(
 185+ 'ContestConfig' => array( 'rules_page' => ContestUtils::getParsedArticleContent( $contest->getField( 'rules_page' ) ) )
 186+ )
 187+ )
 188+ );
 189+ }
 190+
 191+ /**
 192+ * Redirect the user to the contest page and add the "new" argument to the URL
 193+ * so they get a success message.
 194+ *
 195+ * @since 0.1
 196+ *
 197+ * @param Contest $contest
 198+ */
 199+ protected function showSucess( Contest $contest ) {
 200+ $url = SpecialPage::getTitleFor( 'MyContests', $contest->getField( 'name' ) )->getLocalURL( 'new' );
 201+ $this->getOutput()->redirect( $url );
 202+ }
 203+
 204+ /**
 205+ * Gets the field definitions for the form.
 206+ *
 207+ * @since 0.1
 208+ *
 209+ * @param Contest $contest
 210+ * @param integer|false $challengeId
 211+ * @return array
 212+ */
 213+ protected function getFormFields( Contest $contest, $challengeId ) {
 214+ $fields = array();
 215+
 216+ $user = $this->getUser();
 217+
 218+ $fields['contest-id'] = array(
 219+ 'type' => 'hidden',
 220+ 'default' => $contest->getId(),
 221+ 'id' => 'contest-id',
 222+ );
 223+
 224+ $fields['contestant-realname'] = array(
 225+ 'type' => 'text',
 226+ 'default' => $user->getRealName(),
 227+ 'label-message' => 'contest-signup-realname',
 228+ 'required' => true,
 229+ 'validation-callback' => array( __CLASS__, 'validateNameField' )
 230+ );
 231+
 232+ $fields['contestant-email'] = array(
 233+ 'type' => 'text',
 234+ 'default' => $user->getEmail(),
 235+ 'label-message' => 'contest-signup-email',
 236+ 'required' => true,
 237+ 'validation-callback' => array( __CLASS__, 'validateEmailField' )
 238+ );
 239+
 240+ $fields['contestant-country'] = array(
 241+ 'type' => 'select',
 242+ 'label-message' => 'contest-signup-country',
 243+ 'required' => true,
 244+ 'options' => ContestContestant::getCountriesForInput( true ),
 245+ 'validation-callback' => array( __CLASS__, 'validateCountryField' )
 246+ );
 247+
 248+ $fields['contestant-challengeid'] = array(
 249+ 'type' => 'radio',
 250+ 'label-message' => 'contest-signup-challenge',
 251+ 'options' => $this->getChallengesList( $contest ),
 252+ 'required' => true,
 253+ 'validation-callback' => array( __CLASS__, 'validateChallengeField' )
 254+ );
 255+
 256+ if ( $challengeId !== false ) {
 257+ $fields['contestant-challengeid']['default'] = $challengeId;
 258+ }
 259+
 260+ $fields['contestant-volunteer'] = array(
 261+ 'type' => 'check',
 262+ 'default' => '0',
 263+ 'label-message' => 'contest-signup-volunteer',
 264+ );
 265+
 266+ $fields['contestant-wmf'] = array(
 267+ 'type' => 'check',
 268+ 'default' => '0',
 269+ 'label-message' => 'contest-signup-wmf',
 270+ );
 271+
 272+ $fields['contestant-readrules'] = array(
 273+ 'type' => 'check',
 274+ 'default' => '0',
 275+ 'label-message' => array( 'contest-signup-readrules', $contest->getField( 'rules_page' ) ),
 276+ 'validation-callback' => array( __CLASS__, 'validateRulesField' ),
 277+ 'id' => 'contest-rules',
 278+ 'data-foo' => 'bar'
 279+ );
 280+
 281+ return $fields;
 282+ }
 283+
 284+ /**
 285+ * Gets a list of contests that can be fed directly to the options field of
 286+ * an HTMLForm radio input.
 287+ * challenge title => challenge id
 288+ *
 289+ * @since 0.1
 290+ *
 291+ * @param Contest $contest
 292+ *
 293+ * @return array
 294+ */
 295+ protected function getChallengesList( Contest $contest ) {
 296+ $list = array();
 297+
 298+ foreach ( $contest->getChallenges() as /* ContestChallenge */ $challenge ) {
 299+ $list[$challenge->getField( 'title' )] = $challenge->getId();
 300+ }
 301+
 302+ return $list;
 303+ }
 304+
 305+ /**
 306+ * HTMLForm field validation-callback for name field.
 307+ * 1
 308+ * @since 0.1
 309+ *
 310+ * @param $value String
 311+ * @param $alldata Array
 312+ *
 313+ * @return true|string
 314+ */
 315+ public static function validateNameField( $value, $alldata = null ) {
 316+ if ( strlen( $value ) < 2 ) {
 317+ return wfMsg( 'contest-signup-invalid-name' );
 318+ }
 319+
 320+ return true;
 321+ }
 322+
 323+ /**
 324+ * HTMLForm field validation-callback for email field.
 325+ *
 326+ * @since 0.1
 327+ *
 328+ * @param $value String
 329+ * @param $alldata Array
 330+ *
 331+ * @return true|string
 332+ */
 333+ public static function validateEmailField( $value, $alldata = null ) {
 334+ if ( !Sanitizer::validateEmail( $value ) ) {
 335+ return wfMsg( 'contest-signup-invalid-email' );
 336+ }
 337+
 338+ return true;
 339+ }
 340+
 341+ /**
 342+ * HTMLForm field validation-callback for country field.
 343+ *
 344+ * @since 0.1
 345+ *
 346+ * @param $value String
 347+ * @param $alldata Array
 348+ *
 349+ * @return true|string
 350+ */
 351+ public static function validateCountryField( $value, $alldata = null ) {
 352+ if ( $value === '' ) {
 353+ return wfMsg( 'contest-signup-require-country' );
 354+ }
 355+
 356+ return true;
 357+ }
 358+
 359+ /**
 360+ * HTMLForm field validation-callback for rules field.
 361+ *
 362+ * @since 0.1
 363+ *
 364+ * @param $value String
 365+ * @param $alldata Array
 366+ *
 367+ * @return true|string
 368+ */
 369+ public static function validateRulesField( $value, $alldata = null ) {
 370+ if ( !$value ) {
 371+ return wfMsg( 'contest-signup-require-rules' );
 372+ }
 373+
 374+ return true;
 375+ }
 376+
 377+ /**
 378+ * HTMLForm field validation-callback for challenge field.
 379+ *
 380+ * @since 0.1
 381+ *
 382+ * @param $value String
 383+ * @param $alldata Array
 384+ *
 385+ * @return true|string
 386+ */
 387+ public static function validateChallengeField( $value, $alldata = null ) {
 388+ if ( is_null( $value ) ) {
 389+ return wfMsg( 'contest-signup-require-challenge' );
 390+ }
 391+
 392+ return true;
 393+ }
 394+
 395+}
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/specials/SpecialContestSignup.php
___________________________________________________________________
Added: svn:eol-style
1396 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/specials/SpecialEditContest.php
@@ -0,0 +1,402 @@
 2+<?php
 3+
 4+/**
 5+ * Contest editing interface for contest admins.
 6+ *
 7+ * @since 0.1
 8+ *
 9+ * @file SpecialEditContest.php
 10+ * @ingroup Contest
 11+ *
 12+ * @licence GNU GPL v3 or later
 13+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
 14+ */
 15+class SpecialEditContest extends FormSpecialPage {
 16+
 17+ protected $contest = false;
 18+ protected $isNewPost = false;
 19+
 20+ /**
 21+ * Constructor.
 22+ *
 23+ * @since 0.1
 24+ */
 25+ public function __construct() {
 26+ parent::__construct( 'EditContest', 'contestadmin', false );
 27+ }
 28+
 29+ /**
 30+ * @see SpecialPage::getDescription
 31+ *
 32+ * @since 0.1
 33+ */
 34+ public function getDescription() {
 35+ return wfMsg( 'special-' . strtolower( $this->getName() ) );
 36+ }
 37+
 38+ /**
 39+ * Sets headers - this should be called from the execute() method of all derived classes!
 40+ *
 41+ * @since 0.1
 42+ */
 43+ public function setHeaders() {
 44+ $out = $this->getOutput();
 45+ $out->setArticleRelated( false );
 46+ $out->setRobotPolicy( 'noindex,nofollow' );
 47+ $out->setPageTitle( $this->getDescription() );
 48+ }
 49+
 50+ /**
 51+ * Main method.
 52+ *
 53+ * @since 0.1
 54+ *
 55+ * @param string $subPage
 56+ */
 57+ public function execute( $subPage ) {
 58+ $subPage = str_replace( '_', ' ', $subPage );
 59+
 60+ $this->setParameter( $subPage );
 61+ $this->setHeaders();
 62+ $this->outputHeader();
 63+
 64+ // This will throw exceptions if there's a problem
 65+ $this->userCanExecute( $this->getUser() );
 66+
 67+ if ( $this->getRequest()->wasPosted() && $this->getUser()->matchEditToken( $this->getRequest()->getVal( 'wpEditToken' ) ) ) {
 68+ $this->showForm();
 69+ }
 70+ else {
 71+ $this->showContent( $subPage );
 72+ }
 73+ }
 74+
 75+ /**
 76+ * Show the form.
 77+ *
 78+ * @since 0.1
 79+ */
 80+ protected function showForm() {
 81+ $form = $this->getForm();
 82+
 83+ if ( $form->show() ) {
 84+ $this->onSuccess();
 85+ }
 86+ }
 87+
 88+ /**
 89+ * Attempt to get the contest to be edited or create the one to be added.
 90+ * If this works, show the form, if not, redirect to special:contests.
 91+ *
 92+ * @since 0.1
 93+ *
 94+ * @param string $subPage
 95+ */
 96+ protected function showContent( $subPage ) {
 97+ $isNew = $this->getRequest()->wasPosted() && $this->getUser()->matchEditToken( $this->getRequest()->getVal( 'newEditToken' ) );
 98+
 99+ if ( $isNew ) {
 100+ $data = array( 'name' => $this->getRequest()->getVal( 'newcontest' ) );
 101+
 102+ $contest = Contest::s()->selectRow( null, $data );
 103+
 104+ if ( $contest === false ) {
 105+ $contest = new Contest( $data, true );
 106+ }
 107+ else {
 108+ $this->showWarning( 'contest-edit-exists-already' );
 109+ }
 110+ }
 111+ else {
 112+ $contest = Contest::s()->selectRow( null, array( 'name' => $subPage ) );
 113+ }
 114+
 115+ if ( $contest === false ) {
 116+ $this->getOutput()->redirect( SpecialPage::getTitleFor( 'Contests' )->getLocalURL() );
 117+ }
 118+ else {
 119+ if ( !$isNew ) {
 120+ $this->getOutput()->addHTML(
 121+ SpecialContestPage::getNavigation( $contest->getField( 'name' ), $this->getUser(), $this->getLang(), $this->getName() )
 122+ );
 123+ }
 124+
 125+ $this->contest = $contest;
 126+ $this->showForm();
 127+ $this->getOutput()->addModules( 'contest.special.editcontest' );
 128+ }
 129+ }
 130+
 131+ /**
 132+ * (non-PHPdoc)
 133+ * @see FormSpecialPage::getForm()
 134+ * @return HTMLForm|null
 135+ */
 136+ protected function getForm() {
 137+ $form = parent::getForm();
 138+
 139+ $form->addButton(
 140+ 'cancelEdit',
 141+ wfMsg( 'cancel' ),
 142+ 'cancelEdit',
 143+ array(
 144+ 'target-url' => SpecialPage::getTitleFor( 'Contests' )->getFullURL()
 145+ )
 146+ );
 147+
 148+// $form->addButton(
 149+// 'deleteEdit',
 150+// wfMsg( 'delete' ),
 151+// 'deleteEdit'
 152+// );
 153+
 154+ return $form;
 155+ }
 156+
 157+ /**
 158+ * (non-PHPdoc)
 159+ * @see FormSpecialPage::getFormFields()
 160+ * @return array
 161+ */
 162+ protected function getFormFields() {
 163+ $contest = $this->contest;
 164+
 165+ $fields = array();
 166+
 167+ $fields['id'] = array ( 'type' => 'hidden' );
 168+
 169+ $fields['name'] = array (
 170+ 'type' => 'text',
 171+ 'label-message' => 'contest-edit-name',
 172+ 'id' => 'contest-name-field',
 173+ );
 174+
 175+ $fields['status'] = array (
 176+ 'type' => 'radio',
 177+ 'label-message' => 'contest-edit-status',
 178+ 'options' => Contest::getStatusMessages( true )
 179+ );
 180+
 181+ $fields['intro'] = array (
 182+ 'type' => 'text',
 183+ 'label-message' => 'contest-edit-intro',
 184+ );
 185+
 186+ $fields['opportunities'] = array (
 187+ 'type' => 'text',
 188+ 'label-message' => 'contest-edit-opportunities',
 189+ );
 190+
 191+ $fields['rules_page'] = array (
 192+ 'type' => 'text',
 193+ 'label-message' => 'contest-edit-rulespage',
 194+ );
 195+
 196+ $fields['help'] = array (
 197+ 'type' => 'text',
 198+ 'label-message' => 'contest-edit-help',
 199+ );
 200+
 201+ $fields['signup_email'] = array (
 202+ 'type' => 'text',
 203+ 'label-message' => 'contest-edit-signup',
 204+ );
 205+
 206+ $fields['reminder_email'] = array (
 207+ 'type' => 'text',
 208+ 'label-message' => 'contest-edit-reminder',
 209+ );
 210+
 211+ $fields['end'] = array (
 212+ 'type' => 'text',
 213+ 'label-message' => 'contest-edit-end',
 214+ 'id' => 'contest-edit-end',
 215+ 'size' => 15
 216+ );
 217+
 218+ if ( $contest !== false ) {
 219+ foreach ( $fields as $name => $data ) {
 220+ $default = $contest->getField( $name );
 221+
 222+ if ( $name == 'end' ) {
 223+ $default = wfTimestamp( TS_DB, $default );
 224+ }
 225+
 226+ $fields[$name]['default'] = $default;
 227+ }
 228+ }
 229+
 230+ $mappedFields = array();
 231+
 232+ foreach ( $fields as $name => $field ) {
 233+ $mappedFields['contest-' . $name] = $field;
 234+ }
 235+
 236+ if ( $contest !== false ) {
 237+ foreach ( $contest->getChallenges() as /* ContestChallenge */ $challenge ) {
 238+ $mappedFields[] = array(
 239+ 'class' => 'ContestChallengeField',
 240+ 'options' => $challenge->toArray()
 241+ );
 242+ }
 243+ }
 244+
 245+ $mappedFields['delete-challenges'] = array ( 'type' => 'hidden', 'id' => 'delete-challenges' );
 246+
 247+ return $mappedFields;
 248+ }
 249+
 250+ /**
 251+ * Process the form. At this point we know that the user passes all the criteria in
 252+ * userCanExecute(), and if the data array contains 'Username', etc, then Username
 253+ * resets are allowed.
 254+ *
 255+ * @param array $data
 256+ *
 257+ * @return Bool|Array
 258+ */
 259+ public function onSubmit( array $data ) {
 260+ $fields = array();
 261+
 262+ foreach ( $data as $name => $value ) {
 263+ $matches = array();
 264+
 265+ if ( preg_match( '/contest-(.+)/', $name, $matches ) ) {
 266+ if ( $matches[1] == 'end' ) {
 267+ $value = wfTimestamp( TS_MW, strtotime( $value ) );
 268+ }
 269+
 270+ $fields[$matches[1]] = $value;
 271+ }
 272+ }
 273+
 274+ // If no ID is set, this means it's a new contest, so set the ID to null for an insert.
 275+ // However, the user can have hot the back button after creation of a new contest,
 276+ // re-submitting the form. In this case, get the ID of the already existing item for an update.
 277+ if ( !array_key_exists( 'id', $fields ) || $fields['id'] === '' ) {
 278+ $contest = Contest::s()->selectRow( 'id', array( 'name' => $fields['name'] ) );
 279+ $fields['id'] = $contest === false ? null : $contest->getField( 'id' );
 280+ }
 281+
 282+ $contest = new Contest( $fields, true );
 283+
 284+ $contest->setChallenges( $this->getSubmittedChallenges() );
 285+ $success = $contest->writeAllToDB();
 286+
 287+ $success = $this->removeDeletedChallenges( $data['delete-challenges'] ) && $success;
 288+
 289+ if ( $success ) {
 290+ return true;
 291+ }
 292+ else {
 293+ return array(); // TODO
 294+ }
 295+ }
 296+
 297+ /**
 298+ * The UI keeps track of 'removed' challenges by storing them into a
 299+ * hidden HTML input, pipe-separated. On submission, this method
 300+ * takes this string and actually deletes them.
 301+ *
 302+ * @since 0.1
 303+ *
 304+ * @param string $idString
 305+ *
 306+ * @return boolean Success indicator
 307+ */
 308+ protected function removeDeletedChallenges( $idString ) {
 309+ if ( $idString == '' ) {
 310+ return true;
 311+ }
 312+
 313+ return ContestChallenge::s()->delete( array( 'id' => explode( '|', $idString ) ) );
 314+ }
 315+
 316+ /**
 317+ * Finds the submitted challanges and returns them as a list of
 318+ * ContestChallenge objects.
 319+ *
 320+ * @since 0.1
 321+ *
 322+ * @return array of ContestChallenge
 323+ */
 324+ protected function getSubmittedChallenges() {
 325+ $challenges = array();
 326+
 327+ foreach ( $this->getrequest()->getValues() as $name => $value ) {
 328+ $matches = array();
 329+
 330+ if ( preg_match( '/contest-challenge-(\d+)/', $name, $matches ) ) {
 331+ $challenges[] = $this->getSubmittedChallenge( $matches[1] );
 332+ } elseif ( preg_match( '/contest-challenge-new-(\d+)/', $name, $matches ) ) {
 333+ $challenges[] = $this->getSubmittedChallenge( $matches[1], true );
 334+ }
 335+ }
 336+
 337+ return $challenges;
 338+ }
 339+
 340+ /**
 341+ * Create and return a contest challenge object from the submitted data.
 342+ *
 343+ * @since 0.1
 344+ *
 345+ * @param integer|null $challengeId
 346+ *
 347+ * @return ContestChallenge
 348+ */
 349+ protected function getSubmittedChallenge( $challengeId, $isNewChallenge = false ) {
 350+ if ( $isNewChallenge ) {
 351+ $challengeDbId = null;
 352+ $challengeId = "new-$challengeId";
 353+ } else {
 354+ $challengeDbId = $challengeId;
 355+ }
 356+
 357+ $request = $this->getRequest();
 358+
 359+ return new ContestChallenge( array(
 360+ 'id' => $challengeDbId,
 361+ 'text' => $request->getText( "challenge-text-$challengeId" ),
 362+ 'title' => $request->getText( "contest-challenge-$challengeId" ),
 363+ 'oneline' => $request->getText( "challenge-oneline-$challengeId" ),
 364+ ) );
 365+ }
 366+
 367+ public function onSuccess() {
 368+ $this->getOutput()->redirect( SpecialPage::getTitleFor( 'Contests' )->getLocalURL() );
 369+ }
 370+
 371+ /**
 372+ * Show a message in a warning box.
 373+ *
 374+ * @since 0.1
 375+ *
 376+ * @param string $message
 377+ */
 378+ protected function showWarning( $message ) {
 379+ $this->getOutput()->addHTML(
 380+ '<p class="visualClear warningbox">' . wfMsgExt( $message, 'parseinline' ) . '</p>'
 381+ );
 382+ }
 383+
 384+}
 385+
 386+class ContestChallengeField extends HTMLFormField {
 387+
 388+ public function getInputHTML( $value ) {
 389+ $attribs = array(
 390+ 'class' => 'contest-challenge'
 391+ );
 392+
 393+ foreach ( $this->mParams['options'] as $name => $value ) {
 394+ $attribs['data-challenge-' . $name] = $value;
 395+ }
 396+
 397+ return Html::element(
 398+ 'div',
 399+ $attribs
 400+ );
 401+ }
 402+
 403+}
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/specials/SpecialEditContest.php
___________________________________________________________________
Added: svn:eol-style
1404 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/specials/SpecialMyContests.php
@@ -0,0 +1,547 @@
 2+<?php
 3+
 4+/**
 5+ * List of contests for a user.
 6+ *
 7+ * @since 0.1
 8+ *
 9+ * @file SpecialMyContests.php
 10+ * @ingroup Contest
 11+ *
 12+ * @licence GNU GPL v3 or later
 13+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
 14+ */
 15+class SpecialMyContests extends SpecialContestPage {
 16+
 17+ protected $submissionState = null;
 18+
 19+ /**
 20+ * Constructor.
 21+ *
 22+ * @since 0.1
 23+ */
 24+ public function __construct() {
 25+ parent::__construct( 'MyContests', 'contestparticipant' );
 26+ }
 27+
 28+ /**
 29+ * Main method.
 30+ *
 31+ * @since 0.1
 32+ *
 33+ * @param string $arg
 34+ */
 35+ public function execute( $subPage ) {
 36+ $subPage = str_replace( '_', ' ', $subPage );
 37+
 38+ if ( !parent::execute( $subPage ) ) {
 39+ return;
 40+ }
 41+
 42+ if ( $this->getRequest()->wasPosted() ) {
 43+ $contestant = ContestContestant::s()->selectRow( null, array( 'id' => $this->getRequest()->getInt( 'wpcontestant-id' ) ) );
 44+ $this->showSubmissionPage( $contestant );
 45+ }
 46+ else {
 47+ if ( $subPage == '' ) {
 48+ $this->displayContestsOverview();
 49+ }
 50+ else {
 51+ $this->handleSubmissionView( $subPage );
 52+ }
 53+ }
 54+ }
 55+
 56+ /**
 57+ * On regular page view, ie no submission and no sub-page,
 58+ * display a list of all contests the user is participating in,
 59+ * or in case there is only one, redirect them to the submissiom
 60+ * UI of it.
 61+ *
 62+ * @since 0.1
 63+ */
 64+ protected function displayContestsOverview() {
 65+ $contestants = ContestContestant::s()->select(
 66+ array( 'id', 'contest_id', 'challenge_id' ),
 67+ array( 'user_id' => $this->getUser()->getId() )
 68+ );
 69+
 70+ $contestantCount = count( $contestants );
 71+
 72+ if ( $contestantCount == 0 ) {
 73+ $this->getOutput()->addWikiMsg( 'contest-mycontests-no-contests' );
 74+ }
 75+ else if ( $contestantCount == 1 ) {
 76+
 77+ /**
 78+ * @var $contest Contest
 79+ */
 80+ $contest = $contestants[0]->getContest( array( 'status', 'name' ) );
 81+
 82+ if ( $contest->getField( 'status' ) == Contest::STATUS_ACTIVE ) {
 83+ $this->getOutput()->redirect( $this->getTitle( $contest->getField( 'name' ) )->getLocalURL() );
 84+ }
 85+ else {
 86+ $this->displayContestsTable( $contestants );
 87+ }
 88+ }
 89+ else {
 90+ $this->displayContestsTable( $contestants );
 91+ }
 92+ }
 93+
 94+ /**
 95+ * Displays a list of contests the user participates or participated in,
 96+ * together with their user specific choices such as the contest challenge.
 97+ *
 98+ * @since 0.1
 99+ *
 100+ * @param array $contestants
 101+ */
 102+ protected function displayContestsTable( array /* of ContestContestant */ $contestants ) {
 103+ $running = array();
 104+ $passed = array();
 105+ $contests = array();
 106+
 107+ foreach ( $contestants as $contestant ) {
 108+ /**
 109+ * @var $contest Contest
 110+ */
 111+ $contest = $contestant->getContest();
 112+
 113+ if ( $contest->getField( 'status' ) == Contest::STATUS_ACTIVE ) {
 114+ $running[] = $contestant;
 115+ }
 116+ else if ( $contest->getField( 'status' ) == Contest::STATUS_FINISHED ) {
 117+ $passed[] = $contestant;
 118+ }
 119+
 120+ $contests[$contest->getId()] = $contest;
 121+ }
 122+
 123+ if ( count( $running ) > 0 ) {
 124+ $this->displayRunningContests( $running, $contests );
 125+ }
 126+
 127+ if ( count( $passed ) > 0 ) {
 128+ //$this->displayPassedContests( $passed, $contests );
 129+ }
 130+ }
 131+
 132+ /**
 133+ * Display a table with the running (active) contests for this user.
 134+ *
 135+ * @since 0.1
 136+ *
 137+ * @param array $contestants
 138+ * @param array $contests
 139+ */
 140+ protected function displayRunningContests( array /* of ContestContestant */ $contestants, array /* Contest */ $contests ) {
 141+ $out = $this->getOutput();
 142+
 143+ $out->addHTML( Html::element( 'h2', array(), wfMsg( 'contest-mycontests-active-header' ) ) );
 144+ $out->addHTML( Html::element( 'p', array(), wfMsg( 'contest-mycontests-active-text' ) ) );
 145+
 146+ $out->addHTML( Xml::openElement(
 147+ 'table',
 148+ array( 'class' => 'wikitable sortable' )
 149+ ) );
 150+
 151+ $headers = array(
 152+ Html::element( 'th', array(), wfMsg( 'contest-mycontests-header-contest' ) ),
 153+ Html::element( 'th', array(), wfMsg( 'contest-mycontests-header-challenge' ) ),
 154+ );
 155+
 156+ $out->addHTML( '<thead><tr>' . implode( '', $headers ) . '</tr></thead>' );
 157+
 158+ $out->addHTML( '<tbody>' );
 159+
 160+ foreach ( $contestants as $contestant ) {
 161+
 162+ /**
 163+ * @var $contestant ContestContestant
 164+ */
 165+
 166+ /**
 167+ * @var $contest Contest
 168+ */
 169+ $contest = $contests[$contestant->getField( 'contest_id' )];
 170+
 171+ $challengeTitle = ContestChallenge::s()->selectRow(
 172+ 'title',
 173+ array( 'id' => $contestant->getField( 'challenge_id' ) )
 174+ )->getField( 'title' );
 175+
 176+ $fields = array();
 177+
 178+ $fields[] = Html::rawElement( 'td', array( 'data-sort-value' => $contest->getField( 'name' ) ), Html::rawElement(
 179+ 'a',
 180+ array(
 181+ 'href' => SpecialPage::getTitleFor( 'MyContests', $contest->getField( 'name' ) )->getLocalURL()
 182+ ),
 183+ htmlspecialchars( $contest->getField( 'name' ) )
 184+ ) );
 185+
 186+ $fields[] = Html::element( 'td', array(), $challengeTitle );
 187+
 188+ $out->addHTML( '<tr>' . implode( '', $fields ) . '</tr>' );
 189+ }
 190+
 191+ $out->addHTML( '</tbody>' );
 192+ $out->addHTML( '</table>' );
 193+ }
 194+
 195+ /**
 196+ * Display a table with the passed (finished) contests for this user.
 197+ *
 198+ * @since 0.1
 199+ *
 200+ * @param array $contestants
 201+ * @param array $contests
 202+ */
 203+ protected function displayPassedContests( array /* of ContestContestant */ $contestants, array /* Contest */ $contests ) {
 204+ $out = $this->getOutput();
 205+
 206+ $out->addHTML( Html::element( 'h2', array(), wfMsg( 'contest-mycontests-finished-header' ) ) );
 207+ $out->addHTML( Html::element( 'h2', array(), wfMsg( 'contest-mycontests-finished-text' ) ) );
 208+
 209+ // TODO
 210+ }
 211+
 212+ /**
 213+ * Handle view requests for the page.
 214+ *
 215+ * @since 0.1
 216+ *
 217+ * @param string $contestName
 218+ */
 219+ protected function handleSubmissionView( $contestName ) {
 220+ $out = $this->getOutput();
 221+
 222+ $contest = Contest::s()->selectRow( null, array( 'name' => $contestName ) );
 223+
 224+ if ( $contest === false ) {
 225+ $this->showError( 'contest-submission-unknown' );
 226+ $out->addHTML( '<br /><br /><br /><br />' );
 227+ $out->returnToMain();
 228+ }
 229+ else {
 230+ switch ( $contest->getStatus() ) {
 231+ case Contest::STATUS_ACTIVE:
 232+ $this->handleEnabledPage( $contest );
 233+ break;
 234+ case Contest::STATUS_FINISHED:
 235+ case Contest::STATUS_EXPIRED:
 236+ $this->showWarning( 'contest-submission-finished' );
 237+ $out->addHTML( '<br /><br /><br /><br />' );
 238+ $out->returnToMain();
 239+ break;
 240+ }
 241+ }
 242+ }
 243+
 244+ /**
 245+ * Handle page request when the contest is enabled.
 246+ *
 247+ * @since 0.1
 248+ *
 249+ * @param Contest $contest
 250+ */
 251+ protected function handleEnabledPage( Contest $contest ) {
 252+ // Check if the user is already a contestant in this contest.
 253+ // If he is, redirect to submission page, else show signup form.
 254+ $contestant = ContestContestant::s()->selectRow(
 255+ null,
 256+ array(
 257+ 'contest_id' => $contest->getId(),
 258+ 'user_id' => $this->getUser()->getId()
 259+ )
 260+ );
 261+
 262+ if ( $contestant === false ) {
 263+ $this->getOutput()->redirect( SpecialPage::getTitleFor( 'ContestSignup', $contest->getField( 'name' ) )->getLocalURL() );
 264+ }
 265+ else {
 266+ $contestant->setContest( $contest );
 267+ $this->showSubmissionPage( $contestant );
 268+ }
 269+ }
 270+
 271+ /**
 272+ * Show the page content.
 273+ *
 274+ * @since 0.1
 275+ *
 276+ * @param ContestContestant $contestant
 277+ */
 278+ protected function showSubmissionPage( ContestContestant $contestant ) {
 279+ $request = $this->getRequest();
 280+ $contest = $contestant->getContest();
 281+ if ( $request->getCheck( 'new' ) ) {
 282+ $this->showSuccess( 'contest-mycontests-signup-success', $contest->getField( 'name' ) );
 283+ }
 284+ else if ( $request->getCheck( 'added' ) ) {
 285+ $this->showSuccess( 'contest-mycontests-addition-success' );
 286+ }
 287+ else if ( $request->getCheck( 'updated' ) ) {
 288+ $this->showSuccess( 'contest-mycontests-updated-success' );
 289+ }
 290+ else if ( $request->wasPosted()
 291+ && !$this->getUser()->matchEditToken( $request->getVal( 'wpEditToken' ) ) ) {
 292+ $this->showError( 'contest-mycontests-sessionfail' );
 293+ }
 294+
 295+ $output = $this->getOutput();
 296+ $output->setPageTitle( $contest->getField( 'name' ) );
 297+
 298+ $output->addHTML('<div style="clear:both;"></div>');
 299+ $output->addWikiMsg( 'contest-submission-header', $contest->getField( 'name' ) );
 300+
 301+ $form = new HTMLForm( $this->getFormFields( $contestant ), $this->getContext() );
 302+
 303+ $form->setSubmitCallback( array( $this, 'handleSubmission' ) );
 304+ $form->setSubmitText( wfMsg( 'contest-submission-submit' ) );
 305+
 306+ if( $form->show() ) {
 307+ $query = is_null( $this->submissionState ) ? '' : $this->submissionState;
 308+ $output->redirect( $this->getTitle( $contest->getField( 'name' ) )->getLocalURL( $query ) );
 309+ }
 310+ else {
 311+ $output->addModules( 'contest.special.submission' );
 312+ }
 313+ }
 314+
 315+ /**
 316+ * Handle form submission.
 317+ *
 318+ * @since 0.1
 319+ *
 320+ * @return true|array
 321+ */
 322+ public function handleSubmission( array $data ) {
 323+ $user = $this->getUser();
 324+
 325+ $user->setEmail( $data['contestant-email'] );
 326+ $user->setRealName( $data['contestant-realname'] );
 327+ $user->saveSettings();
 328+
 329+ $contestant = new ContestContestant( array(
 330+ 'id' => $data['contestant-id'],
 331+
 332+ 'full_name' => $data['contestant-realname'],
 333+ 'email' => $data['contestant-email'],
 334+
 335+ 'country' => $data['contestant-country'],
 336+ 'volunteer' => $data['contestant-volunteer'],
 337+ 'wmf' => $data['contestant-wmf'],
 338+ 'cv' => $data['contestant-cv'],
 339+
 340+ 'submission' => trim( $data['contestant-submission'] ),
 341+ ) );
 342+
 343+ $success = $contestant->writeToDB();
 344+
 345+ if ( $success ) {
 346+ if ( trim( $data['contestant-previous-submission'] ) === '' && trim( $data['contestant-submission'] ) !== '' ) {
 347+ $this->submissionState = 'added';
 348+ }
 349+ else {
 350+ $this->submissionState = 'updated';
 351+ }
 352+ }
 353+
 354+ return $success;
 355+ }
 356+
 357+ /**
 358+ * Gets the field definitions for the form.
 359+ *
 360+ * @since 0.1
 361+ *
 362+ * @param ContestContestant $contest
 363+ */
 364+ protected function getFormFields( ContestContestant $contestant ) {
 365+ $fields = array();
 366+
 367+ $user = $this->getUser();
 368+
 369+ $fields['contestant-id'] = array(
 370+ 'type' => 'hidden',
 371+ 'default' => $contestant->getId(),
 372+ 'id' => 'contest-id',
 373+ );
 374+
 375+ $fields['contestant-previous-submission'] = array(
 376+ 'type' => 'hidden',
 377+ 'default' => $contestant->getField( 'submission' ),
 378+ );
 379+
 380+ $fields['contestant-submission'] = array(
 381+ 'class' => 'ContestSubmissionField',
 382+ 'label-message' => 'contest-submission-submission',
 383+ 'validation-callback' => array( __CLASS__, 'validateSubmissionField' ),
 384+ 'options' => array(
 385+ 'domains' => implode( '|', ContestSettings::get( 'submissionDomains' ) ),
 386+ 'value' => $contestant->getField( 'submission' )
 387+ )
 388+ );
 389+
 390+ $fields['contestant-realname'] = array(
 391+ 'type' => 'text',
 392+ 'default' => $user->getRealName(),
 393+ 'label-message' => 'contest-signup-realname',
 394+ 'required' => true,
 395+ 'validation-callback' => array( __CLASS__, 'validateNameField' )
 396+ );
 397+
 398+ $fields['contestant-email'] = array(
 399+ 'type' => 'email',
 400+ 'default' => $user->getEmail(),
 401+ 'label-message' => 'contest-signup-email',
 402+ 'required' => true,
 403+ 'validation-callback' => array( __CLASS__, 'validateEmailField' ),
 404+ );
 405+
 406+ $fields['contestant-country'] = array(
 407+ 'type' => 'select',
 408+ 'default' => $contestant->getField( 'country' ),
 409+ 'label-message' => 'contest-signup-country',
 410+ 'required' => true,
 411+ 'options' => ContestContestant::getCountriesForInput()
 412+ );
 413+
 414+ $fields['contestant-volunteer'] = array(
 415+ 'type' => 'check',
 416+ 'default' => $contestant->getField( 'volunteer' ),
 417+ 'label-message' => 'contest-signup-volunteer',
 418+ );
 419+
 420+ $fields['contestant-wmf'] = array(
 421+ 'type' => 'check',
 422+ 'default' => $contestant->getField( 'wmf' ),
 423+ 'label-message' => 'contest-signup-wmf',
 424+ );
 425+
 426+ $hasWMF = $contestant->hasField( 'wmf' );
 427+
 428+ $fields['contestant-cv'] = array(
 429+ 'type' => $hasWMF && $contestant->getField( 'wmf' ) ? 'text' : 'hidden',
 430+ 'default' => $hasWMF ? $contestant->getField( 'cv' ) : '',
 431+ 'label-message' => 'contest-signup-cv',
 432+ 'validation-callback' => array( __CLASS__, 'validateCVField' ),
 433+ );
 434+
 435+ return $fields;
 436+ }
 437+
 438+ /**
 439+ * HTMLForm field validation-callback for name field.
 440+ *
 441+ * @since 0.1
 442+ *
 443+ * @param $value String
 444+ * @param $alldata Array
 445+ *
 446+ * @return true|string
 447+ */
 448+ public static function validateNameField( $value, $alldata = null ) {
 449+ if ( strlen( $value ) < 2 ) {
 450+ return wfMsg( 'contest-signup-invalid-name' );
 451+ }
 452+
 453+ return true;
 454+ }
 455+
 456+ /**
 457+ * HTMLForm field validation-callback for email field.
 458+ *
 459+ * @since 0.1
 460+ *
 461+ * @param $value String
 462+ * @param $alldata Array
 463+ *
 464+ * @return true|string
 465+ */
 466+ public static function validateEmailField( $value, $alldata = null ) {
 467+ if ( !Sanitizer::validateEmail( $value ) ) {
 468+ return wfMsg( 'contest-signup-invalid-email' );
 469+ }
 470+
 471+ return true;
 472+ }
 473+
 474+ /**
 475+ * HTMLForm field validation-callback for cv field.
 476+ *
 477+ * @since 0.1
 478+ *
 479+ * @param $value String
 480+ * @param $alldata Array
 481+ *
 482+ * @return true|string
 483+ */
 484+ public static function validateCVField( $value, $alldata = null ) {
 485+ if ( trim( $value ) !== '' && filter_var( $value, FILTER_VALIDATE_URL ) === false ) {
 486+ return wfMsg( 'contest-signup-invalid-cv' );
 487+ }
 488+
 489+ return true;
 490+ }
 491+
 492+ /**
 493+ * HTMLForm field validation-callback for the submissiom field.
 494+ * Warning: regexes used! o_O
 495+ *
 496+ * @since 0.1
 497+ *
 498+ * @param $value String
 499+ * @param $alldata Array
 500+ *
 501+ * @return true|string
 502+ */
 503+ public static function validateSubmissionField( $value, $alldata = null ) {
 504+ $value = trim( $value );
 505+
 506+ if ( $value == '' ) {
 507+ return true;
 508+ }
 509+
 510+ $allowedPatterns = array(
 511+ // GitHub URLs such as https://github.com/JeroenDeDauw/smwcon/tree/f9b26ec4ba1101b1f5d4ef76b7ae6ad3dabfb53b
 512+ // '@^https://github\.com/[a-zA-Z0-9-]+/[a-zA-Z0-9_-]+/tree/[a-zA-Z0-9]{40}$@i'
 513+ );
 514+
 515+ foreach ( ContestSettings::get( 'submissionDomains' ) as $domain ) {
 516+ $allowedPatterns[] = '@^https?://(([a-z0-9]+)\.)?' . str_replace( '.', '\.', $domain ) . '/.*$@i';
 517+ }
 518+
 519+ foreach ( $allowedPatterns as $pattern ) {
 520+ if ( preg_match( $pattern, $value ) ) {
 521+ return true;
 522+ }
 523+ }
 524+
 525+ return wfMsg( 'contest-submission-invalid-url' );
 526+ }
 527+
 528+}
 529+
 530+class ContestSubmissionField extends HTMLFormField {
 531+
 532+ public function getInputHTML( $value ) {
 533+ $attribs = array(
 534+ 'class' => 'contest-submission',
 535+ 'data-name' => $this->mName
 536+ );
 537+
 538+ foreach ( $this->mParams['options'] as $name => $value ) {
 539+ $attribs['data-' . $name] = $value;
 540+ }
 541+
 542+ return Html::element(
 543+ 'div',
 544+ $attribs
 545+ );
 546+ }
 547+
 548+}
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/specials/SpecialMyContests.php
___________________________________________________________________
Added: svn:eol-style
1549 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/Contest.settings.php
@@ -0,0 +1,80 @@
 2+<?php
 3+
 4+/**
 5+ * File defining the settings for the Contest extension.
 6+ * More info can be found at https://www.mediawiki.org/wiki/Extension:Contest#Settings
 7+ *
 8+ * NOTICE:
 9+ * Changing one of these settings can be done by assigning to $egContestSettings,
 10+ * AFTER the inclusion of the extension itself.
 11+ *
 12+ * @since 0.1
 13+ *
 14+ * @file Contest.settings.php
 15+ * @ingroup Contest
 16+ *
 17+ * @licence GNU GPL v3+
 18+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
 19+ */
 20+class ContestSettings {
 21+
 22+ /**
 23+ * Returns the default values for the settings.
 24+ * setting name (string) => setting value (mixed)
 25+ *
 26+ * @since 0.1
 27+ *
 28+ * @return array
 29+ */
 30+ protected static function getDefaultSettings() {
 31+ return array(
 32+ 'voteValues' => range( 0, 5 ),
 33+ 'enableTopLink' => true,
 34+ 'submissionDomains' => array( 'github.com', 'gitorious.org', 'mediawiki.org' ),
 35+ 'reminderJobSize' => 50
 36+ );
 37+ }
 38+
 39+ /**
 40+ * Retruns an array with all settings after making sure they are
 41+ * initialized (ie set settings have been merged with the defaults).
 42+ * setting name (string) => setting value (mixed)
 43+ *
 44+ * @since 0.1
 45+ *
 46+ * @return array
 47+ */
 48+ public static function getSettings() {
 49+ static $settings = false;
 50+
 51+ if ( $settings === false ) {
 52+ $settings = array_merge(
 53+ self::getDefaultSettings(),
 54+ $GLOBALS['egContestSettings']
 55+ );
 56+ }
 57+
 58+ return $settings;
 59+ }
 60+
 61+ /**
 62+ * Gets the value of the specified setting.
 63+ *
 64+ * @since 0.1
 65+ *
 66+ * @param string $settingName
 67+ *
 68+ * @throws MWException
 69+ * @return mixed
 70+ */
 71+ public static function get( $settingName ) {
 72+ $settings = self::getSettings();
 73+
 74+ if ( !array_key_exists( $settingName, $settings ) ) {
 75+ throw new MWException( 'Attempt to get non-existing setting "' . $settingName . '"' );
 76+ }
 77+
 78+ return $settings[$settingName];
 79+ }
 80+
 81+}
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/Contest.settings.php
___________________________________________________________________
Added: svn:eol-style
182 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/Contest.php
@@ -0,0 +1,277 @@
 2+<?php
 3+
 4+/**
 5+ * Initialization file for the Contest extension.
 6+ *
 7+ * Documentation: https://www.mediawiki.org/wiki/Extension:Contest
 8+ * Support https://www.mediawiki.org/wiki/Extension_talk:Contest
 9+ * Source code: http://svn.wikimedia.org/viewvc/mediawiki/trunk/extensions/Contest
 10+ *
 11+ * @file Contest.php
 12+ * @ingroup Contest
 13+ *
 14+ * @licence GNU GPL v3+
 15+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
 16+ */
 17+
 18+/**
 19+ * This documentation group collects source code files belonging to Contest.
 20+ *
 21+ * @defgroup Contest Contest
 22+ */
 23+
 24+if ( !defined( 'MEDIAWIKI' ) ) {
 25+ die( 'Not an entry point.' );
 26+}
 27+
 28+if ( version_compare( $wgVersion, '1.18c', '<' ) ) { // Needs to be 1.18c because version_compare() works in confusing ways
 29+ die( '<b>Error:</b> Contest requires MediaWiki 1.18 or above.' );
 30+}
 31+
 32+define( 'CONTEST_VERSION', '0.1alpha' );
 33+
 34+$wgExtensionCredits['other'][] = array(
 35+ 'path' => __FILE__,
 36+ 'name' => 'Contest',
 37+ 'version' => CONTEST_VERSION,
 38+ 'author' => array(
 39+ '[http://www.mediawiki.org/wiki/User:Jeroen_De_Dauw Jeroen De Dauw]',
 40+ ),
 41+ 'url' => 'http://www.mediawiki.org/wiki/Extension:Contest',
 42+ 'descriptionmsg' => 'contest-desc'
 43+);
 44+
 45+// i18n
 46+$wgExtensionMessagesFiles['Contest'] = dirname( __FILE__ ) . '/Contest.i18n.php';
 47+$wgExtensionMessagesFiles['ContestAlias'] = dirname( __FILE__ ) . '/Contest.alias.php';
 48+
 49+// Autoloading
 50+$wgAutoloadClasses['ContestHooks'] = dirname( __FILE__ ) . '/Contest.hooks.php';
 51+$wgAutoloadClasses['ContestSettings'] = dirname( __FILE__ ) . '/Contest.settings.php';
 52+
 53+$wgAutoloadClasses['ApiContestQuery'] = dirname( __FILE__ ) . '/api/ApiContestQuery.php';
 54+$wgAutoloadClasses['ApiDeleteContest'] = dirname( __FILE__ ) . '/api/ApiDeleteContest.php';
 55+$wgAutoloadClasses['ApiMailContestants'] = dirname( __FILE__ ) . '/api/ApiMailContestants.php';
 56+$wgAutoloadClasses['ApiQueryChallenges'] = dirname( __FILE__ ) . '/api/ApiQueryChallenges.php';
 57+$wgAutoloadClasses['ApiQueryContestants'] = dirname( __FILE__ ) . '/api/ApiQueryContestants.php';
 58+$wgAutoloadClasses['ApiQueryContestComments'] = dirname( __FILE__ ) . '/api/ApiQueryContestComments.php';
 59+$wgAutoloadClasses['ApiQueryContests'] = dirname( __FILE__ ) . '/api/ApiQueryContests.php';
 60+
 61+$wgAutoloadClasses['Contest'] = dirname( __FILE__ ) . '/includes/Contest.class.php';
 62+$wgAutoloadClasses['ContestantPager'] = dirname( __FILE__ ) . '/includes/ContestantPager.php';
 63+$wgAutoloadClasses['ContestChallenge'] = dirname( __FILE__ ) . '/includes/ContestChallenge.php';
 64+$wgAutoloadClasses['ContestComment'] = dirname( __FILE__ ) . '/includes/ContestComment.php';
 65+$wgAutoloadClasses['ContestContestant'] = dirname( __FILE__ ) . '/includes/ContestContestant.php';
 66+$wgAutoloadClasses['ContestDBObject'] = dirname( __FILE__ ) . '/includes/ContestDBObject.php';
 67+$wgAutoloadClasses['ContestReminderJob'] = dirname( __FILE__ ) . '/includes/ContestReminderJob.php';
 68+$wgAutoloadClasses['ContestUtils'] = dirname( __FILE__ ) . '/includes/ContestUtils.php';
 69+$wgAutoloadClasses['ContestVote'] = dirname( __FILE__ ) . '/includes/ContestVote.php';
 70+
 71+$wgAutoloadClasses['SpecialContest'] = dirname( __FILE__ ) . '/specials/SpecialContest.php';
 72+$wgAutoloadClasses['SpecialContestant'] = dirname( __FILE__ ) . '/specials/SpecialContestant.php';
 73+$wgAutoloadClasses['SpecialContestPage'] = dirname( __FILE__ ) . '/specials/SpecialContestPage.php';
 74+$wgAutoloadClasses['SpecialContests'] = dirname( __FILE__ ) . '/specials/SpecialContests.php';
 75+$wgAutoloadClasses['SpecialContestSignup'] = dirname( __FILE__ ) . '/specials/SpecialContestSignup.php';
 76+$wgAutoloadClasses['SpecialContestWelcome'] = dirname( __FILE__ ) . '/specials/SpecialContestWelcome.php';
 77+$wgAutoloadClasses['SpecialEditContest'] = dirname( __FILE__ ) . '/specials/SpecialEditContest.php';
 78+$wgAutoloadClasses['SpecialMyContests'] = dirname( __FILE__ ) . '/specials/SpecialMyContests.php';
 79+
 80+// Special pages
 81+$wgSpecialPages['Contest'] = 'SpecialContest';
 82+$wgSpecialPages['Contestant'] = 'SpecialContestant';
 83+$wgSpecialPages['Contests'] = 'SpecialContests';
 84+$wgSpecialPages['ContestSignup'] = 'SpecialContestSignup';
 85+$wgSpecialPages['ContestWelcome'] = 'SpecialContestWelcome';
 86+$wgSpecialPages['EditContest'] = 'SpecialEditContest';
 87+$wgSpecialPages['MyContests'] = 'SpecialMyContests';
 88+
 89+$wgSpecialPageGroups['Contest'] = 'contest';
 90+$wgSpecialPageGroups['Contestant'] = 'contest';
 91+$wgSpecialPageGroups['Contests'] = 'contest';
 92+$wgSpecialPageGroups['ContestSignup'] = 'contest';
 93+$wgSpecialPageGroups['ContestWelcome'] = 'contest';
 94+$wgSpecialPageGroups['EditContest'] = 'contest';
 95+$wgSpecialPageGroups['MyContests'] = 'contest';
 96+
 97+// API
 98+$wgAPIModules['deletecontest'] = 'ApiDeleteContest';
 99+$wgAPIModules['mailcontestants'] = 'ApiMailContestants';
 100+$wgAPIListModules['challenges'] = 'ApiQueryChallenges';
 101+$wgAPIListModules['contestants'] = 'ApiQueryContestants';
 102+$wgAPIListModules['contestcomments'] = 'ApiQueryContestComments';
 103+$wgAPIListModules['contests'] = 'ApiQueryContests';
 104+
 105+// Hooks
 106+$wgHooks['LoadExtensionSchemaUpdates'][] = 'ContestHooks::onSchemaUpdate';
 107+$wgHooks['UnitTestsList'][] = 'ContestHooks::registerUnitTests';
 108+$wgHooks['UserSetEmail'][] = 'ContestHooks::onUserSetEmail';
 109+$wgHooks['PersonalUrls'][] = 'ContestHooks::onPersonalUrls';
 110+$wgHooks['GetPreferences'][] = 'ContestHooks::onGetPreferences';
 111+$wgHooks['LinkEnd'][] = 'ContestHooks::onLinkEnd';
 112+
 113+// Rights
 114+
 115+$wgAvailableRights[] = 'contestadmin';
 116+$wgAvailableRights[] = 'contestparticipant';
 117+$wgAvailableRights[] = 'contestjudge';
 118+
 119+# Users that can manage the contests.
 120+$wgGroupPermissions['*' ]['contestadmin'] = false;
 121+//$wgGroupPermissions['user' ]['contestadmin'] = false;
 122+//$wgGroupPermissions['autoconfirmed']['contestadmin'] = false;
 123+//$wgGroupPermissions['bot' ]['contestadmin'] = false;
 124+$wgGroupPermissions['sysop' ]['contestadmin'] = true;
 125+$wgGroupPermissions['contestadmin' ]['contestadmin'] = true;
 126+
 127+# Users that can be contest participants.
 128+$wgGroupPermissions['*' ]['contestparticipant'] = false;
 129+$wgGroupPermissions['user' ]['contestparticipant'] = true;
 130+//$wgGroupPermissions['autoconfirmed']['contestparticipant'] = true;
 131+//$wgGroupPermissions['bot' ]['contestparticipant'] = false;
 132+$wgGroupPermissions['sysop' ]['contestparticipant'] = true;
 133+$wgGroupPermissions['contestparticipant']['contestparticipant'] = true;
 134+
 135+# Users that can vote and comment on submissions.
 136+$wgGroupPermissions['*' ]['contestjudge'] = false;
 137+//$wgGroupPermissions['user' ]['contestjudge'] = false;
 138+//$wgGroupPermissions['autoconfirmed']['contestjudge'] = false;
 139+//$wgGroupPermissions['bot' ]['contestjudge'] = false;
 140+$wgGroupPermissions['sysop' ]['contestjudge'] = true;
 141+$wgGroupPermissions['contestjudge' ]['contestjudge'] = true;
 142+
 143+
 144+// Resource loader modules
 145+$moduleTemplate = array(
 146+ 'localBasePath' => dirname( __FILE__ ) . '/resources',
 147+ 'remoteExtPath' => 'Contest/resources'
 148+);
 149+
 150+$wgResourceModules['contest.special.contests'] = $moduleTemplate + array(
 151+ 'scripts' => array(
 152+ 'contest.special.contests.js'
 153+ ),
 154+ 'messages' => array(
 155+ 'contest-special-confirm-delete',
 156+ 'contest-special-delete-failed',
 157+ )
 158+);
 159+
 160+$wgResourceModules['jquery.ui.timepicker'] = $moduleTemplate + array(
 161+ 'scripts' => array(
 162+ 'jquery.ui.timepicker.js',
 163+ ),
 164+ 'styles' => array(
 165+ 'jquery.ui.timepicker.css',
 166+ ),
 167+ 'dependencies' => array(
 168+ 'jquery.ui.slider',
 169+ 'jquery.ui.datepicker'
 170+ )
 171+);
 172+
 173+$wgResourceModules['contest.special.editcontest'] = $moduleTemplate + array(
 174+ 'scripts' => array(
 175+ 'contest.special.editcontest.js',
 176+ ),
 177+ 'messages' => array(
 178+ 'contest-edit-delete',
 179+ 'contest-edit-add-first',
 180+ 'contest-edit-add-another',
 181+ 'contest-edit-confirm-delete',
 182+ 'contest-edit-challenge-title',
 183+ 'contest-edit-challenge-text',
 184+ 'contest-edit-challenge-oneline',
 185+ ),
 186+ 'dependencies' => array(
 187+ 'jquery.ui.button',
 188+ 'jquery.ui.timepicker'
 189+ )
 190+);
 191+
 192+$wgResourceModules['jquery.contestChallenges'] = $moduleTemplate + array(
 193+ 'scripts' => array(
 194+ 'jquery.contestChallenges.js'
 195+ ),
 196+ 'messages' => array(
 197+ 'contest-welcome-accept-challenge'
 198+ ),
 199+ 'dependencies' => array(
 200+ 'jquery.ui.button'
 201+ )
 202+);
 203+
 204+$wgResourceModules['contest.special.welcome'] = $moduleTemplate + array(
 205+ 'scripts' => array(
 206+ 'contest.special.welcome.js'
 207+ ),
 208+ 'styles' => array(
 209+ 'contest.special.welcome.css',
 210+ ),
 211+ 'dependencies' => array(
 212+ 'jquery.contestChallenges', 'jquery.fancybox',
 213+ ),
 214+ 'messages' => array(
 215+ 'contest-welcome-select-header',
 216+ 'contest-welcome-rules',
 217+ 'contest-welcome-rules-link',
 218+ )
 219+);
 220+
 221+$wgResourceModules['contest.special.signup'] = $moduleTemplate + array(
 222+ 'scripts' => array(
 223+ 'contest.special.signup.js',
 224+ ),
 225+ 'dependencies' => array(
 226+ 'jquery.ui.button', 'jquery.fancybox',
 227+ )
 228+);
 229+
 230+$wgResourceModules['contest.special.submission'] = $moduleTemplate + array(
 231+ 'scripts' => array(
 232+ 'contest.special.submission.js',
 233+ ),
 234+ 'dependencies' => array(
 235+ 'jquery.ui.button', 'jquery.contestSubmission',
 236+ ),
 237+);
 238+
 239+$wgResourceModules['jquery.contestSubmission'] = $moduleTemplate + array(
 240+ 'scripts' => array(
 241+ 'jquery.contestSubmission.js',
 242+ ),
 243+ 'messages' => array(
 244+ 'contest-submission-new-submission',
 245+ 'contest-submission-current-submission',
 246+ 'contest-submission-domains',
 247+ )
 248+);
 249+
 250+$wgResourceModules['contest.contestant.pager'] = $moduleTemplate + array(
 251+ 'scripts' => array(
 252+ 'contest.contestant.pager.js',
 253+ ),
 254+ 'styles' => array(
 255+ 'contest.contestant.pager.css',
 256+ ),
 257+);
 258+
 259+$wgResourceModules['contest.special.contestant'] = $moduleTemplate + array(
 260+ 'styles' => array(
 261+ 'contest.special.contestant.css',
 262+ ),
 263+);
 264+
 265+$wgResourceModules['jquery.fancybox'] = $moduleTemplate + array(
 266+ 'scripts' => array(
 267+ 'fancybox/jquery.fancybox-1.3.4.js',
 268+ ),
 269+ 'styles' => array(
 270+ 'fancybox/jquery.fancybox-1.3.4.css'
 271+ ),
 272+);
 273+
 274+unset( $moduleTemplate );
 275+
 276+$egContestSettings = array();
 277+
 278+$wgContestEmailParse = false;
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/Contest.php
___________________________________________________________________
Added: svn:eol-style
1279 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/INSTALL
@@ -0,0 +1,64 @@
 2+These is the install file for the Contest extension.
 3+
 4+Extension page on mediawiki.org: https://www.mediawiki.org/wiki/Extension:Contest
 5+Latest version of the install file: http://svn.wikimedia.org/viewvc/mediawiki/trunk/extensions/Contest/INSTALL?view=co
 6+
 7+
 8+== Requirements ==
 9+
 10+Contest requires:
 11+
 12+* MediaWiki 1.18 or above
 13+* PHP 5.2 or above
 14+* MySQL
 15+
 16+== Download ==
 17+
 18+You can get the code directly from SVN. Tags can be obtained via
 19+
 20+ svn checkout <nowiki>http://svn.wikimedia.org/svnroot/mediawiki/tags/extensions/Contest/REL_version</nowiki>
 21+
 22+Where 'version' is the version number of the tag, such as 0_1 (see the [http://svn.wikimedia.org/svnroot/mediawiki/tags/extensions/Contest/ available tags]).
 23+The latest code can be obtained from trunk:
 24+
 25+ svn checkout <nowiki>http://svn.wikimedia.org/svnroot/mediawiki/trunk/extensions/Contest/</nowiki>
 26+
 27+== Installation ==
 28+
 29+Once you have downloaded the code, place the ''Contest'' directory within your MediaWiki
 30+'extensions' directory. Then add the following code to your [[Manual:LocalSettings.php|LocalSettings.php]] file:
 31+
 32+# Contest
 33+require_once( "$IP/extensions/Contest/Contest.php" );
 34+
 35+== Configuration ==
 36+
 37+Configuration of Contest is done by assigning to $egContestSettings in your
 38+[[Manual:LocalSettings.php|LocalSettings.php]] file, AFTER the inclusion of the
 39+extension. The options are listed below and their default is set in the Contest settings file:
 40+http://svn.wikimedia.org/viewvc/mediawiki/trunk/extensions/Contest/Contest.settings.php?view=markup
 41+You should NOT modify the settings file, but can have a look at it to get an idea of
 42+how to use the settings, in case the below descriptions do not suffice.
 43+
 44+{| class="wikitable sortable"
 45+! Name
 46+! Type
 47+! Default
 48+! Description
 49+|-
 50+| votevalues
 51+| array of integer
 52+| range( 0, 5 )
 53+| Values that can be voted by judges on a participant
 54+|-
 55+| enableTopLink
 56+| boolean
 57+| True
 58+| Enable display of the top link to Special:MyContests
 59+|-
 60+| submissionDomains
 61+| array of string
 62+| array( 'github.com', 'gitorious.org' )
 63+| Domains on which submissions can be placed
 64+|}
 65+
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/INSTALL
___________________________________________________________________
Added: svn:eol-style
166 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/RELEASE-NOTES
@@ -0,0 +1,17 @@
 2+These are the release notes for the Contest extension.
 3+
 4+Extension page on mediawiki.org: https://www.mediawiki.org/wiki/Extension:Contest
 5+Latest version of the release notes: http://svn.wikimedia.org/viewvc/mediawiki/trunk/extensions/Contest/RELEASE-NOTES?view=co
 6+
 7+
 8+=== Version 0.1 ===
 9+2011-10-xx
 10+
 11+Initial release with these features:
 12+
 13+* Admin interface for managing contests and their challenges.
 14+* Landing and signup pages for each contest.
 15+* Personal contest list and submission interface for each user.
 16+* Summary pages per contest listing contestants, which can be filtered and sorted.
 17+* Judging interface that allows for rating and commenting on each participant.
 18+* All contests, challenges, contestants, comments and votes can be queried and exported via the API.
\ No newline at end of file
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/RELEASE-NOTES
___________________________________________________________________
Added: svn:eol-style
119 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/includes/ContestDBObject.php
@@ -0,0 +1,805 @@
 2+<?php
 3+
 4+/**
 5+ * Abstract base class for representing objects that are stored in some DB table.
 6+ * This is a modified copy of SurveyDBClass, backported to work with PHP 5.2,
 7+ * and therefore missing all the awesome you get with late static binding.
 8+ *
 9+ * @since 0.1
 10+ *
 11+ * @file ContestDBObject.php
 12+ * @ingroup Contest
 13+ *
 14+ * @licence GNU GPL v3 or later
 15+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
 16+ */
 17+abstract class ContestDBObject {
 18+
 19+ /**
 20+ * The fields of the object.
 21+ * field name (w/o prefix) => value
 22+ *
 23+ * @since 0.1
 24+ * @var array
 25+ */
 26+ protected $fields = array( 'id' => null );
 27+
 28+ /**
 29+ * Constructor.
 30+ *
 31+ * @since 0.1
 32+ *
 33+ * @param array|null $fields
 34+ * @param boolean $loadDefaults
 35+ */
 36+ public function __construct( $fields, $loadDefaults = false ) {
 37+ if ( !is_array( $fields ) ) {
 38+ $fields = array();
 39+ }
 40+
 41+ if ( $loadDefaults ) {
 42+ $fields = array_merge( $this->getDefaults(), $fields );
 43+ }
 44+
 45+ $this->setFields( $fields );
 46+ }
 47+
 48+ /**
 49+ * Load the specified fields from the database.
 50+ *
 51+ * @since 0.1
 52+ *
 53+ * @param array|null $fields
 54+ * @param boolean $override
 55+ *
 56+ * @return Success indicator
 57+ */
 58+ public function loadFields( $fields = null, $override = true ) {
 59+ if ( is_null( $this->getId() ) ) {
 60+ return false;
 61+ }
 62+
 63+ if ( is_null( $fields ) ) {
 64+ $fields = array_keys( $this->getFieldTypes() );
 65+ }
 66+
 67+ $results = $this->rawSelect(
 68+ $this->getPrefixedFields( $fields ),
 69+ array( $this->getPrefixedField( 'id' ) => $this->getId() ),
 70+ array( 'LIMIT' => 1 )
 71+ );
 72+
 73+ foreach ( $results as $result ) {
 74+ $this->setFields( $this->getFieldsFromDBResult( $result ), $override );
 75+ return true;
 76+ }
 77+
 78+ return false;
 79+ }
 80+
 81+ /**
 82+ * Returns the name of the database table objects of this type are stored in.
 83+ *
 84+ * @since 0.1
 85+ *
 86+ * @return string
 87+ */
 88+ public abstract function getDBTable();
 89+
 90+ /**
 91+ * Gets the db field prefix.
 92+ *
 93+ * @since 0.1
 94+ *
 95+ * @return string
 96+ */
 97+ protected abstract function getFieldPrefix();
 98+
 99+ /**
 100+ * Gets the value of a field.
 101+ *
 102+ * @since 0.1
 103+ *
 104+ * @param string $name
 105+ *
 106+ * @throws MWException
 107+ * @return mixed
 108+ */
 109+ public function getField( $name ) {
 110+ if ( $this->hasField( $name ) ) {
 111+ return $this->fields[$name];
 112+ } else {
 113+ throw new MWException( 'Attempted to get not-set field ' . $name );
 114+ }
 115+ }
 116+
 117+ /**
 118+ * Remove a field.
 119+ *
 120+ * @since 0.1
 121+ *
 122+ * @param string $name
 123+ */
 124+ public function removeField( $name ) {
 125+ unset( $this->fields[$name] );
 126+ }
 127+
 128+ /**
 129+ * Returns the objects database id.
 130+ *
 131+ * @since 0.1
 132+ *
 133+ * @return integer|null
 134+ */
 135+ public function getId() {
 136+ return $this->getField( 'id' );
 137+ }
 138+
 139+ /**
 140+ * Sets the objects database id.
 141+ *
 142+ * @since 0.1
 143+ *
 144+ * @param integere|null $id
 145+ */
 146+ public function setId( $id ) {
 147+ return $this->setField( 'id', $id );
 148+ }
 149+
 150+ /**
 151+ * Gets if a certain field is set.
 152+ *
 153+ * @since 0.1
 154+ *
 155+ * @param string $name
 156+ *
 157+ * @return boolean
 158+ */
 159+ public function hasField( $name ) {
 160+ return array_key_exists( $name, $this->fields );
 161+ }
 162+
 163+ /**
 164+ * Gets if the id field is set.
 165+ *
 166+ * @since 0.1
 167+ *
 168+ * @return boolean
 169+ */
 170+ public function hasIdField() {
 171+ return $this->hasField( 'id' )
 172+ && !is_null( $this->getField( 'id' ) );
 173+ }
 174+
 175+ /**
 176+ * Sets multiple fields.
 177+ *
 178+ * @since 0.1
 179+ *
 180+ * @param array $fields The fields to set
 181+ * @param boolean $override Override already set fields with the provided values?
 182+ */
 183+ public function setFields( array $fields, $override = true ) {
 184+ foreach ( $fields as $name => $value ) {
 185+ if ( $override || !$this->hasField( $name ) ) {
 186+ $this->setField( $name, $value );
 187+ }
 188+ }
 189+ }
 190+
 191+ /**
 192+ * Gets the fields => values to write to the table.
 193+ *
 194+ * @since 0.1
 195+ *
 196+ * @return array
 197+ */
 198+ protected function getWriteValues() {
 199+ $values = array();
 200+
 201+ foreach ( $this->getFieldTypes() as $name => $type ) {
 202+ if ( array_key_exists( $name, $this->fields ) ) {
 203+ $value = $this->fields[$name];
 204+
 205+ switch ( $type ) {
 206+ case 'array':
 207+ $value = serialize( (array)$value );
 208+ }
 209+
 210+ $values[$this->getFieldPrefix() . $name] = $value;
 211+ }
 212+ }
 213+
 214+ return $values;
 215+ }
 216+
 217+ /**
 218+ * Serializes the object to an associative array which
 219+ * can then easily be converted into JSON or similar.
 220+ *
 221+ * @since 0.1
 222+ *
 223+ * @param null|array $props
 224+ * @param boolean $incNullId
 225+ *
 226+ * @return array
 227+ */
 228+ public function toArray( $fields = null, $incNullId = false ) {
 229+ $data = array();
 230+ $setFields = array();
 231+
 232+ if ( !is_array( $fields ) ) {
 233+ $setFields = $this->getSetFieldNames();
 234+ } else {
 235+ foreach ( $fields as $field ) {
 236+ if ( $this->hasField( $field ) ) {
 237+ $setFields[] = $field;
 238+ }
 239+ }
 240+ }
 241+
 242+ foreach ( $setFields as $field ) {
 243+ if ( $incNullId || $field != 'id' || $this->hasIdField() ) {
 244+ $data[$field] = $this->getField( $field );
 245+ }
 246+ }
 247+
 248+ return $data;
 249+ }
 250+
 251+ /**
 252+ * Load the default values, via getDefaults.
 253+ *
 254+ * @since 0.1
 255+ *
 256+ * @param boolean $override
 257+ */
 258+ public function loadDefaults( $override = true ) {
 259+ $this->setFields( $this->getDefaults(), $override );
 260+ }
 261+
 262+ /**
 263+ * Writes the answer to the database, either updating it
 264+ * when it already exists, or inserting it when it doesn't.
 265+ *
 266+ * @since 0.1
 267+ *
 268+ * @return boolean Success indicator
 269+ */
 270+ public function writeToDB() {
 271+ if ( $this->hasIdField() ) {
 272+ return $this->updateInDB();
 273+ } else {
 274+ return $this->insertIntoDB();
 275+ }
 276+ }
 277+
 278+ /**
 279+ * Updates the object in the database.
 280+ *
 281+ * @since 0.1
 282+ *
 283+ * @return boolean Success indicator
 284+ */
 285+ protected function updateInDB() {
 286+ $dbw = wfGetDB( DB_MASTER );
 287+
 288+ return $dbw->update(
 289+ $this->getDBTable(),
 290+ $this->getWriteValues(),
 291+ array( $this->getFieldPrefix() . 'id' => $this->getId() ),
 292+ __METHOD__
 293+ );
 294+ }
 295+
 296+ /**
 297+ * Inserts the object into the database.
 298+ *
 299+ * @since 0.1
 300+ *
 301+ * @return boolean Success indicator
 302+ */
 303+ protected function insertIntoDB() {
 304+ $dbw = wfGetDB( DB_MASTER );
 305+
 306+ $result = $dbw->insert(
 307+ $this->getDBTable(),
 308+ $this->getWriteValues(),
 309+ __METHOD__,
 310+ array( 'IGNORE' )
 311+ );
 312+
 313+ $this->setField( 'id', $dbw->insertId() );
 314+
 315+ return $result;
 316+ }
 317+
 318+ /**
 319+ * Removes the object from the database.
 320+ *
 321+ * @since 0.1
 322+ *
 323+ * @return boolean Success indicator
 324+ */
 325+ public function removeFromDB() {
 326+ $sucecss = $this->delete( array( 'id' => $this->getId() ) );
 327+
 328+ if ( $sucecss ) {
 329+ $this->setField( 'id', null );
 330+ }
 331+
 332+ return $sucecss;
 333+ }
 334+
 335+ /**
 336+ * Return the names of the fields.
 337+ *
 338+ * @since 0.1
 339+ *
 340+ * @return array
 341+ */
 342+ public function getFields() {
 343+ return $this->fields;
 344+ }
 345+
 346+ /**
 347+ * Return the names of the fields.
 348+ *
 349+ * @since 0.1
 350+ *
 351+ * @return array
 352+ */
 353+ public function getSetFieldNames() {
 354+ return array_keys( $this->fields );
 355+ }
 356+
 357+ /**
 358+ * Sets the value of a field.
 359+ * Strings can be provided for other types,
 360+ * so this method can be called from unserialization handlers.
 361+ *
 362+ * @since 0.1
 363+ *
 364+ * @param string $name
 365+ * @param mixed $value
 366+ *
 367+ * @throws MWException
 368+ */
 369+ public function setField( $name, $value ) {
 370+ $fields = $this->getFieldTypes();
 371+
 372+ if ( array_key_exists( $name, $fields ) ) {
 373+ switch ( $fields[$name] ) {
 374+ case 'int':
 375+ $value = (int)$value;
 376+ break;
 377+ case 'float':
 378+ $value = (float)$value;
 379+ break;
 380+ case 'bool':
 381+ if ( is_string( $value ) ) {
 382+ $value = $value !== '0';
 383+ } elseif ( is_int( $value ) ) {
 384+ $value = $value !== 0;
 385+ }
 386+ break;
 387+ case 'array':
 388+ if ( is_string( $value ) ) {
 389+ $value = unserialize( $value );
 390+ }
 391+ break;
 392+ case 'id':
 393+ if ( is_string( $value ) ) {
 394+ $value = (int)$value;
 395+ }
 396+ break;
 397+ }
 398+
 399+ $this->fields[$name] = $value;
 400+ } else {
 401+ throw new MWException( 'Attempted to set unknown field ' . $name );
 402+ }
 403+ }
 404+
 405+ /**
 406+ * Returns an array with the fields and their types this object contains.
 407+ * This corresponds directly to the fields in the database, without prefix.
 408+ *
 409+ * field name => type
 410+ *
 411+ * Allowed types:
 412+ * * id
 413+ * * str
 414+ * * int
 415+ * * float
 416+ * * bool
 417+ * * array
 418+ *
 419+ * @since 0.1
 420+ *
 421+ * @return array
 422+ */
 423+ protected abstract function getFieldTypes();
 424+
 425+ /**
 426+ * Returns a list of default field values.
 427+ * field name => field value
 428+ *
 429+ * @since 0.1
 430+ *
 431+ * @return array
 432+ */
 433+ public abstract function getDefaults();
 434+
 435+ //
 436+ //
 437+ // All below methods ought to be static, but can't be since this would require LSB introduced in PHP 5.3.
 438+ //
 439+ //
 440+
 441+ /**
 442+ * Gets if the object can take a certain field.
 443+ *
 444+ * @since 0.1
 445+ *
 446+ * @param string $name
 447+ *
 448+ * @return boolean
 449+ */
 450+ public function canHasField( $name ) {
 451+ return array_key_exists( $name, $this->getFieldTypes() );
 452+ }
 453+
 454+ /**
 455+ * Takes in a field or array of fields and returns an
 456+ * array with their prefixed versions, ready for db usage.
 457+ *
 458+ * @since 0.1
 459+ *
 460+ * @param array|string $fields
 461+ *
 462+ * @return array
 463+ */
 464+ public function getPrefixedFields( $fields ) {
 465+ $fields = (array)$fields;
 466+
 467+ foreach ( $fields as &$field ) {
 468+ $field = $this->getFieldPrefix() . $field;
 469+ }
 470+
 471+ return $fields;
 472+ }
 473+
 474+ /**
 475+ * Takes in a field and returns an it's prefixed version, ready for db usage.
 476+ *
 477+ * @since 0.1
 478+ *
 479+ * @param string $field
 480+ *
 481+ * @return string
 482+ */
 483+ public function getPrefixedField( $field ) {
 484+ return $this->getFieldPrefix() . $field;
 485+ }
 486+
 487+ /**
 488+ * Takes in an associative array with field names as keys and
 489+ * their values as value. The field names are prefixed with the
 490+ * db field prefix.
 491+ *
 492+ * @since 0.1
 493+ *
 494+ * @param array $values
 495+ *
 496+ * @return array
 497+ */
 498+ public function getPrefixedValues( array $values ) {
 499+ $prefixedValues = array();
 500+
 501+ foreach ( $values as $field => $value ) {
 502+ $prefixedValues[$this->getFieldPrefix() . $field] = $value;
 503+ }
 504+
 505+ return $prefixedValues;
 506+ }
 507+
 508+ /**
 509+ * Get an array with fields from a database result,
 510+ * that can be fed directly to the constructor or
 511+ * to setFields.
 512+ *
 513+ * @since 0.1
 514+ *
 515+ * @param object $result
 516+ *
 517+ * @return array
 518+ */
 519+ protected function getFieldsFromDBResult( $result ) {
 520+ $result = (array)$result;
 521+ $data = array();
 522+ $idFieldLength = strlen( $this->getFieldPrefix() );
 523+
 524+ foreach ( $result as $name => $value ) {
 525+ $data[substr( $name, $idFieldLength )] = $value;
 526+ }
 527+
 528+ return $data;
 529+ }
 530+
 531+ /**
 532+ * Get a new instance of the class from a database result.
 533+ *
 534+ * @since 0.1
 535+ *
 536+ * @param object $result
 537+ *
 538+ * @return ContestDBObject
 539+ */
 540+ public function newFromDBResult( $result ) {
 541+ return $this->newFromArray( $this->getFieldsFromDBResult( $result ) );
 542+ }
 543+
 544+ /**
 545+ * Removes the object from the database.
 546+ *
 547+ * @since 0.1
 548+ *
 549+ * @param array $conditions
 550+ *
 551+ * @return boolean Success indicator
 552+ */
 553+ public function delete( array $conditions ) {
 554+ return wfGetDB( DB_MASTER )->delete(
 555+ $this->getDBTable(),
 556+ $this->getPrefixedValues( $conditions )
 557+ );
 558+ }
 559+
 560+ /**
 561+ * Add an amount (can be negative) to the specified field (needs to be numeric).
 562+ *
 563+ * @since 0.1
 564+ *
 565+ * @param string $field
 566+ * @param integer $amount
 567+ *
 568+ * @return boolean Success indicator
 569+ */
 570+ public function addToField( $field, $amount ) {
 571+ if ( $amount == 0 ) {
 572+ return true;
 573+ }
 574+
 575+ if ( !$this->hasIdField() ) {
 576+ return false;
 577+ }
 578+
 579+ $absoluteAmount = abs( $amount );
 580+ $isNegative = $amount < 0;
 581+
 582+ $dbw = wfGetDB( DB_MASTER );
 583+
 584+ $fullField = $this->getPrefixedField( $field );
 585+
 586+ $success = $dbw->update(
 587+ $this->getDBTable(),
 588+ array( "$fullField=$fullField" . ( $isNegative ? '-' : '+' ) . $absoluteAmount ),
 589+ array( $this->getPrefixedField( 'id' ) => $this->getId() ),
 590+ __METHOD__
 591+ );
 592+
 593+ if ( $success && $this->hasField( $field ) ) {
 594+ $this->setField( $field, $this->getField( $field ) + $amount );
 595+ }
 596+
 597+ return $success;
 598+ }
 599+
 600+ /**
 601+ * Selects the the specified fields of the records matching the provided
 602+ * conditions. Field names get prefixed.
 603+ *
 604+ * @since 0.1
 605+ *
 606+ * @param array|string|null $fields
 607+ * @param array $conditions
 608+ * @param array $options
 609+ *
 610+ * @return array of self
 611+ */
 612+ public function select( $fields = null, array $conditions = array(), array $options = array() ) {
 613+ if ( is_null( $fields ) ) {
 614+ $fields = array_keys( $this->getFieldTypes() );
 615+ }
 616+
 617+ $result = $this->rawSelect(
 618+ $this->getPrefixedFields( $fields ),
 619+ $this->getPrefixedValues( $conditions ),
 620+ $options
 621+ );
 622+
 623+ $objects = array();
 624+
 625+ foreach ( $result as $record ) {
 626+ $objects[] = $this->newFromDBResult( $record );
 627+ }
 628+
 629+ return $objects;
 630+ }
 631+
 632+ /**
 633+ * Selects the the specified fields of the first matching record.
 634+ * Field names get prefixed.
 635+ *
 636+ * @since 0.1
 637+ *
 638+ * @param array|string|null $fields
 639+ * @param array $conditions
 640+ * @param array $options
 641+ *
 642+ * @return self|false
 643+ */
 644+ public function selectRow( $fields = null, array $conditions = array(), array $options = array() ) {
 645+ $options['LIMIT'] = 1;
 646+
 647+ $objects = $this->select( $fields, $conditions, $options );
 648+
 649+ return count( $objects ) > 0 ? $objects[0] : false;
 650+ }
 651+
 652+ /**
 653+ * Returns if there is at least one record matching the provided conditions.
 654+ * Condition field names get prefixed.
 655+ *
 656+ * @since 0.1
 657+ *
 658+ * @param array $conditions
 659+ *
 660+ * @return boolean
 661+ */
 662+ public function has( array $conditions = array() ) {
 663+ return $this->selectRow( array( 'id' ), $conditions ) !== false;
 664+ }
 665+
 666+ /**
 667+ * Returns the amount of matching records.
 668+ * Condition field names get prefixed.
 669+ *
 670+ * @since 0.1
 671+ *
 672+ * @param array $conditions
 673+ * @param array $options
 674+ *
 675+ * @return integer
 676+ */
 677+ public function count( array $conditions = array(), array $options = array() ) {
 678+ $res = $this->rawSelect(
 679+ array( 'COUNT(*) AS rowcount' ),
 680+ $this->getPrefixedValues( $conditions ),
 681+ $options
 682+ )->fetchObject();
 683+
 684+ return $res->rowcount;
 685+ }
 686+
 687+ /**
 688+ * Selects the the specified fields of the records matching the provided
 689+ * conditions. Field names do NOT get prefixed.
 690+ *
 691+ * @since 0.1
 692+ *
 693+ * @param array|null $fields
 694+ * @param array $conditions
 695+ * @param array $options
 696+ *
 697+ * @return ResultWrapper
 698+ */
 699+ public function rawSelect( $fields = null, array $conditions = array(), array $options = array() ) {
 700+ $dbr = wfGetDB( DB_SLAVE );
 701+
 702+ return $dbr->select(
 703+ $this->getDBTable(),
 704+ $fields,
 705+ count( $conditions ) == 0 ? '' : $conditions,
 706+ __METHOD__,
 707+ $options
 708+ );
 709+ }
 710+
 711+ /**
 712+ * Update the records matching the provided conditions by
 713+ * setting the fields that are keys in the $values patam to
 714+ * their corresponding values.
 715+ *
 716+ * @since 0.1
 717+ *
 718+ * @param array $values
 719+ * @param array $conditions
 720+ *
 721+ * @return boolean Success indicator
 722+ */
 723+ public function update( array $values, array $conditions = array() ) {
 724+ $dbw = wfGetDB( DB_MASTER );
 725+
 726+ return $dbw->update(
 727+ $this->getDBTable(),
 728+ $this->getPrefixedValues( $values ),
 729+ $this->getPrefixedValues( $conditions ),
 730+ __METHOD__
 731+ );
 732+ }
 733+
 734+ /**
 735+ * Return the names of the fields.
 736+ *
 737+ * @since 0.1
 738+ *
 739+ * @return array
 740+ */
 741+ public function getFieldNames() {
 742+ return array_keys( $this->getFieldTypes() );
 743+ }
 744+
 745+ /**
 746+ * Returns an array with the fields and their descriptions.
 747+ *
 748+ * field name => field description
 749+ *
 750+ * @since 0.1
 751+ *
 752+ * @return array
 753+ */
 754+ public function getFieldDescriptions() {
 755+ return array();
 756+ }
 757+
 758+ /**
 759+ * Get API parameters for the fields supported by this object.
 760+ *
 761+ * @since 0.1
 762+ *
 763+ * @param boolean $requireParams
 764+ * @param boolean $setDefaults
 765+ *
 766+ * @return array
 767+ */
 768+ public function getAPIParams( $requireParams = false, $setDefaults = false ) {
 769+ $typeMap = array(
 770+ 'id' => 'integer',
 771+ 'int' => 'integer',
 772+ 'float' => 'NULL',
 773+ 'str' => 'string',
 774+ 'bool' => 'integer',
 775+ 'array' => 'string'
 776+ );
 777+
 778+ $params = array();
 779+ $defaults = $this->getDefaults();
 780+
 781+ foreach ( $this->getFieldTypes() as $field => $type ) {
 782+ if ( $field == 'id' ) {
 783+ continue;
 784+ }
 785+
 786+ $hasDefault = array_key_exists( $field, $defaults );
 787+
 788+ $params[$field] = array(
 789+ ApiBase::PARAM_TYPE => $typeMap[$type],
 790+ ApiBase::PARAM_REQUIRED => $requireParams && !$hasDefault
 791+ );
 792+
 793+ if ( $type == 'array' ) {
 794+ $params[$field][ApiBase::PARAM_ISMULTI] = true;
 795+ }
 796+
 797+ if ( $setDefaults && $hasDefault ) {
 798+ $default = is_array( $defaults[$field] ) ? implode( '|', $defaults[$field] ) : $defaults[$field];
 799+ $params[$field][ApiBase::PARAM_DFLT] = $default;
 800+ }
 801+ }
 802+
 803+ return $params;
 804+ }
 805+
 806+}
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/includes/ContestDBObject.php
___________________________________________________________________
Added: svn:eol-style
1807 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/includes/ContestVote.php
@@ -0,0 +1,121 @@
 2+<?php
 3+
 4+/**
 5+ * Class representing a single contest vote.
 6+ * Votes can be made by judges on (submissions of) contestants.
 7+ *
 8+ * @since 0.1
 9+ *
 10+ * @file ContestComment.php
 11+ * @ingroup Contest
 12+ *
 13+ * @licence GNU GPL v3 or later
 14+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
 15+ */
 16+class ContestVote extends ContestDBObject {
 17+
 18+ /**
 19+ * Method to get an instance so methods that ought to be static,
 20+ * but can't be due to PHP 5.2 not having LSB, can be called on
 21+ * it. This also allows easy identifying of code that needs to
 22+ * be changed once PHP 5.3 becomes an acceptable requirement.
 23+ *
 24+ * @since 0.1
 25+ *
 26+ * @return ContestDBObject
 27+ */
 28+ public static function s() {
 29+ static $instance = false;
 30+
 31+ if ( $instance === false ) {
 32+ $instance = new self( array() );
 33+ }
 34+
 35+ return $instance;
 36+ }
 37+
 38+ /**
 39+ * Get a new instance of the class from an array.
 40+ * This method ought to be in the basic class and
 41+ * return a new static(), but this requires LSB/PHP>=5.3.
 42+ *
 43+ * @since 0.1
 44+ *
 45+ * @param array $data
 46+ * @param boolean $loadDefaults
 47+ *
 48+ * @return ContestDBObject
 49+ */
 50+ public function newFromArray( array $data, $loadDefaults = false ) {
 51+ return new self( $data, $loadDefaults );
 52+ }
 53+
 54+ /**
 55+ * @see parent::getFieldTypes
 56+ *
 57+ * @since 0.1
 58+ *
 59+ * @return string
 60+ */
 61+ public function getDBTable() {
 62+ return 'contest_votes';
 63+ }
 64+
 65+ /**
 66+ * @see parent::getFieldTypes
 67+ *
 68+ * @since 0.1
 69+ *
 70+ * @return string
 71+ */
 72+ protected function getFieldPrefix() {
 73+ return 'vote_';
 74+ }
 75+
 76+ /**
 77+ * @see parent::getFieldTypes
 78+ *
 79+ * @since 0.1
 80+ *
 81+ * @return array
 82+ */
 83+ protected function getFieldTypes() {
 84+ return array(
 85+ 'id' => 'id',
 86+ 'contestant_id' => 'id',
 87+ 'user_id' => 'id',
 88+
 89+ 'value' => 'int',
 90+ );
 91+ }
 92+
 93+ /**
 94+ * @see parent::getDefaults
 95+ *
 96+ * @since 0.1
 97+ *
 98+ * @return array
 99+ */
 100+ public function getDefaults() {
 101+ return array(
 102+ );
 103+ }
 104+
 105+ /**
 106+ * (non-PHPdoc)
 107+ * @see ContestDBObject::writeToDB()
 108+ * @return bool
 109+ */
 110+ public function writeToDB() {
 111+ $success = parent::writeToDB();
 112+
 113+ if ( $success ) {
 114+ $contestant = new ContestContestant( array( 'id' => $this->getField( 'contestant_id' ) ) );
 115+ $contestant->updateVotes();
 116+ $contestant->writeToDB();
 117+ }
 118+
 119+ return $success;
 120+ }
 121+
 122+}
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/includes/ContestVote.php
___________________________________________________________________
Added: svn:eol-style
1123 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/includes/ContestReminderJob.php
@@ -0,0 +1,45 @@
 2+<?php
 3+
 4+/**
 5+ * Contest reminder job for email reminders.
 6+ *
 7+ * @since 0.1
 8+ *
 9+ * @file ContestReminderJob.php
 10+ * @ingroup Contest
 11+ *
 12+ * @licence GNU GPL v3 or later
 13+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
 14+ */
 15+class ContestReminderJob extends Job {
 16+
 17+ /**
 18+ * Constructor.
 19+ *
 20+ * @param Title $title
 21+ * @param array $params
 22+ * * contestants, array of ContestContestant, required
 23+ * * contest, Contest, required
 24+ */
 25+ public function __construct( Title $title, array $params ) {
 26+ parent::__construct( 'ContestReminderJob', $title, $params );
 27+ $this->params['emailText'] = ContestUtils::getParsedArticleContent( $this->params['reminder_email'] );
 28+ $this->params['daysLeft'] = $this->params['contest']->getDaysLeft();
 29+ }
 30+
 31+ /**
 32+ * Execute the job.
 33+ *
 34+ * @return bool
 35+ */
 36+ public function run() {
 37+ foreach ( $this->params['contestants'] as /* ContestContestant */ $contestant ) {
 38+ $contestant->sendReminderEmail( $this->params['emailText'], array(
 39+ 'daysLeft' => $this->params['daysLeft'],
 40+ ) );
 41+ }
 42+
 43+ return true;
 44+ }
 45+
 46+}
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/includes/ContestReminderJob.php
___________________________________________________________________
Added: svn:eol-style
147 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/includes/ContestUtils.php
@@ -0,0 +1,71 @@
 2+<?php
 3+
 4+/**
 5+ * Utility functions for the Contest extension.
 6+ *
 7+ * @since 0.1
 8+ *
 9+ * @file ContestUtils.php
 10+ * @ingroup Contest
 11+ *
 12+ * @licence GNU GPL v3 or later
 13+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
 14+ */
 15+class ContestUtils {
 16+
 17+ /**
 18+ * Gets the content of the article with the provided page name,
 19+ * or an empty string when there is no such article.
 20+ *
 21+ * @since 0.1
 22+ *
 23+ * @param string $pageName
 24+ *
 25+ * @return string
 26+ */
 27+ public static function getArticleContent( $pageName ) {
 28+ $title = Title::newFromText( $pageName );
 29+
 30+ if ( is_null( $title ) ) {
 31+ return '';
 32+ }
 33+
 34+ $article = new Article( $title, 0 );
 35+ return $article->fetchContent();
 36+ }
 37+
 38+ /**
 39+ * Gets the content of the article with the provided page name,
 40+ * or an empty string when there is no such article.
 41+ *
 42+ * @since 0.1
 43+ *
 44+ * @param string $pageName
 45+ *
 46+ * @return string
 47+ */
 48+ public static function getParsedArticleContent( $pageName ) {
 49+ $title = Title::newFromText( $pageName );
 50+
 51+ if ( is_null( $title ) ) {
 52+ return '';
 53+ }
 54+
 55+ $article = new Article( $title, 0 );
 56+
 57+ global $wgParser, $wgContestEmailParse;
 58+
 59+ $wgContestEmailParse = true;
 60+
 61+ $text = $wgParser->parse(
 62+ $article->fetchContent(),
 63+ $article->getTitle(),
 64+ $article->getParserOptions()
 65+ )->getText();
 66+
 67+ $wgContestEmailParse = false;
 68+
 69+ return $text;
 70+ }
 71+
 72+}
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/includes/ContestUtils.php
___________________________________________________________________
Added: svn:eol-style
173 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/includes/ContestantPager.php
@@ -0,0 +1,283 @@
 2+<?php
 3+
 4+/**
 5+ * Contestant pager, for on Special:Contest
 6+ *
 7+ * @since 0.1
 8+ *
 9+ * @file ContestantPager.php
 10+ * @ingroup Contest
 11+ *
 12+ * @licence GNU GPL v3 or later
 13+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
 14+ */
 15+class ContestantPager extends TablePager {
 16+
 17+ /**
 18+ * Query conditions, full field names (ie inc prefix).
 19+ * @var array
 20+ */
 21+ protected $conds;
 22+
 23+ /**
 24+ * Special page on which the pager is displayed.
 25+ * @var SpecialContestPage
 26+ */
 27+ protected $page;
 28+
 29+ /**
 30+ * Cache for challenge titles.
 31+ * challenge id => challenge title
 32+ * @var array
 33+ */
 34+ protected $challengeTitles = array();
 35+
 36+ /**
 37+ * Constructor.
 38+ *
 39+ * @param SpecialContestPage $page
 40+ * @param array $conds
 41+ */
 42+ public function __construct( SpecialContestPage $page, array $conds ) {
 43+ $this->page = $page;
 44+ $this->conds = $conds;
 45+ $this->mDefaultDirection = true;
 46+
 47+ $this->queryChallengeTitles( $conds );
 48+
 49+ // when MW 1.19 becomes min, we want to pass an IContextSource $context here.
 50+ parent::__construct();
 51+
 52+ $this->getOutput()->addModules( 'contest.contestant.pager' );
 53+ }
 54+
 55+ /**
 56+ * Query all challenge names we might need,
 57+ * based on the queries conditions, and set them
 58+ * to the challengeTitles field.
 59+ *
 60+ * @since 0.1
 61+ *
 62+ * @param array $allConds
 63+ */
 64+ protected function queryChallengeTitles( array $allConds ) {
 65+ $conds = array();
 66+
 67+ if ( array_key_exists( 'contestant_contest_id', $allConds ) ) {
 68+ $conds['contest_id'] = $allConds['contestant_contest_id'];
 69+ }
 70+
 71+ if ( array_key_exists( 'contestant_challenge_id', $allConds ) ) {
 72+ $conds['id'] = $allConds['contestant_challenge_id'];
 73+ }
 74+
 75+ foreach ( ContestChallenge::s()->select( array( 'id', 'title' ), $conds ) as /* ContestChallenge */ $challenge ) {
 76+ $this->challengeTitles[$challenge->getId()] = $challenge->getField( 'title' );
 77+ }
 78+ }
 79+
 80+ /**
 81+ * Gets the title of a challenge given it's id.
 82+ *
 83+ * @since 0.1
 84+ *
 85+ * @param integer $challengeId
 86+ * @throws MWException
 87+ */
 88+ protected function getChallengeTitle( $challengeId ) {
 89+ if ( array_key_exists( $challengeId, $this->challengeTitles ) ) {
 90+ return $this->challengeTitles[$challengeId];
 91+ }
 92+ else {
 93+ throw new MWException( 'Attempt to get non-set challenge title' );
 94+ }
 95+ }
 96+
 97+ /**
 98+ * Get the OutputPage being used for this instance.
 99+ * IndexPager extends ContextSource as of 1.19.
 100+ *
 101+ * @since 0.1
 102+ *
 103+ * @return OutputPage
 104+ */
 105+ public function getOutput() {
 106+ return version_compare( $GLOBALS['wgVersion'], '1.18', '>' ) ? parent::getOutput() : $GLOBALS['wgOut'];
 107+ }
 108+
 109+ /**
 110+ * Get the Language being used for this instance.
 111+ * IndexPager extends ContextSource as of 1.19.
 112+ *
 113+ * @since 0.1
 114+ *
 115+ * @return Language
 116+ */
 117+ public function getLang() {
 118+ return version_compare( $GLOBALS['wgVersion'], '1.18', '>' ) ? parent::getLang() : $GLOBALS['wgLang'];
 119+ }
 120+
 121+ /**
 122+ * @return array
 123+ */
 124+ public function getFieldNames() {
 125+ static $headers = null;
 126+
 127+ if ( is_null( $headers ) ) {
 128+ $headers = array(
 129+ 'contestant_id' => 'contest-contestant-id',
 130+ 'contestant_challenge_id' => 'contest-contestant-challenge-name',
 131+ 'contestant_volunteer' => 'contest-contestant-volunteer',
 132+ 'contestant_wmf' => 'contest-contestant-wmf',
 133+ 'contestant_comments' => 'contest-contestant-commentcount',
 134+ 'contestant_rating' => 'contest-contestant-overallrating',
 135+ );
 136+
 137+ $headers = array_map( 'wfMsg', $headers );
 138+ }
 139+
 140+ return $headers;
 141+ }
 142+
 143+ /**
 144+ * @param $row
 145+ * @return string
 146+ */
 147+ function formatRow( $row ) {
 148+ $this->mCurrentRow = $row; # In case formatValue etc need to know
 149+ $s = Xml::openElement( 'tr', $this->getRowAttrs($row) );
 150+
 151+ foreach ( $this->getFieldNames() as $field => $name ) {
 152+ $value = isset( $row->$field ) ? $row->$field : null;
 153+ $formatted = strval( $this->formatValue( $field, $value ) );
 154+
 155+ if ( $formatted == '' ) {
 156+ $formatted = '&#160;';
 157+ }
 158+ $s .= Xml::tags( 'td', $this->getCellAttrs( $field, $value ), $formatted );
 159+ }
 160+ $s .= "</tr>\n";
 161+
 162+ return $s;
 163+ }
 164+
 165+ /**
 166+ * @param $row
 167+ * @return array
 168+ */
 169+ function getRowAttrs( $row ) {
 170+ return array_merge(
 171+ parent::getRowAttrs( $row ),
 172+ array( 'data-contestant-target' => SpecialPage::getTitleFor( 'Contestant', $row->contestant_id )->getLocalURL() )
 173+ );
 174+ }
 175+
 176+ /**
 177+ * @param $row
 178+ * @return string
 179+ */
 180+ function getRowClass( $row ) {
 181+ return 'contestant-row';
 182+ }
 183+
 184+ /**
 185+ * @param $name
 186+ * @param $value
 187+ * @return string
 188+ */
 189+ public function formatValue( $name, $value ) {
 190+ switch ( $name ) {
 191+ case 'contestant_id':
 192+ $value = Html::element(
 193+ 'a',
 194+ array(
 195+ 'href' => SpecialPage::getTitleFor( 'Contestant', $value )->getLocalURL()
 196+ ),
 197+ $value
 198+ );
 199+ break;
 200+ case 'contestant_challenge_id':
 201+ $value = Html::element(
 202+ 'a',
 203+ array(
 204+ 'href' =>
 205+ SpecialPage::getTitleFor(
 206+ 'Contest',
 207+ $this->page->subPage . '/' . $this->getChallengeTitle( $value )
 208+ )->getLocalURL()
 209+ ),
 210+ $this->getChallengeTitle( $value )
 211+ );
 212+ break;
 213+ case 'contestant_volunteer': case 'contestant_wmf':
 214+ // contest-contestant-yes, contest-contestant-no
 215+ $value = htmlspecialchars( wfMsg( 'contest-contestant-' . ( $value === '1' ? 'yes' : 'no' ) ) );
 216+ break;
 217+ case 'contestant_comments':
 218+ $value = htmlspecialchars( $this->getLang()->formatNum( $value ) );
 219+ break;
 220+ case 'contestant_rating':
 221+ $value = htmlspecialchars( wfMsgExt(
 222+ 'contest-contestant-rating',
 223+ 'parsemag',
 224+ $this->getLang()->formatNum( $value ),
 225+ $this->getLang()->formatNum( $this->mCurrentRow->contestant_rating_count )
 226+ ) );
 227+ break;
 228+ }
 229+
 230+ return $value;
 231+ }
 232+
 233+ function getQueryInfo() {
 234+ $info = array(
 235+ 'tables' => array( 'contest_contestants' ),
 236+ 'fields' => array(
 237+ 'contestant_id',
 238+ 'contestant_challenge_id',
 239+ 'contestant_volunteer',
 240+ 'contestant_wmf',
 241+ 'contestant_comments',
 242+ 'contestant_rating',
 243+ 'contestant_rating_count',
 244+ ),
 245+ 'conds' => $this->conds,
 246+ );
 247+
 248+ return $info;
 249+ }
 250+
 251+ public function getTableClass(){
 252+ return 'TablePager contest-contestants';
 253+ }
 254+
 255+ function getIndexField() {
 256+ return 'contestant_id';
 257+ }
 258+
 259+ function getDefaultSort() {
 260+ return 'contestant_id';
 261+ }
 262+
 263+ function isFieldSortable( $name ) {
 264+ return in_array(
 265+ $name,
 266+ array(
 267+ 'contestant_id',
 268+ 'contestant_challenge_id',
 269+ 'contestant_volunteer',
 270+ 'contestant_wmf',
 271+ 'contestant_comments',
 272+ 'contestant_rating',
 273+ )
 274+ );
 275+ }
 276+
 277+ /**
 278+ * @return Title
 279+ */
 280+ function getTitle() {
 281+ return $this->page->getFullTitle();
 282+ }
 283+
 284+}
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/includes/ContestantPager.php
___________________________________________________________________
Added: svn:eol-style
1285 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/includes/ContestChallenge.php
@@ -0,0 +1,126 @@
 2+<?php
 3+
 4+/**
 5+ * Class representing a single contest challenge object.
 6+ * Each contest (can) has a list of associated challenges.
 7+ *
 8+ * @since 0.1
 9+ *
 10+ * @file ContestChallenge.php
 11+ * @ingroup Contest
 12+ *
 13+ * @licence GNU GPL v3 or later
 14+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
 15+ */
 16+class ContestChallenge extends ContestDBObject {
 17+
 18+ /**
 19+ * Method to get an instance so methods that ought to be static,
 20+ * but can't be due to PHP 5.2 not having LSB, can be called on
 21+ * it. This also allows easy identifying of code that needs to
 22+ * be changed once PHP 5.3 becomes an acceptable requirement.
 23+ *
 24+ * @since 0.1
 25+ *
 26+ * @return ContestDBObject
 27+ */
 28+ public static function s() {
 29+ static $instance = false;
 30+
 31+ if ( $instance === false ) {
 32+ $instance = new self( array() );
 33+ }
 34+
 35+ return $instance;
 36+ }
 37+
 38+ /**
 39+ * Get a new instance of the class from an array.
 40+ * This method ought to be in the basic class and
 41+ * return a new static(), but this requires LSB/PHP>=5.3.
 42+ *
 43+ * @since 0.1
 44+ *
 45+ * @param array $data
 46+ * @param boolean $loadDefaults
 47+ *
 48+ * @return ContestDBObject
 49+ */
 50+ public function newFromArray( array $data, $loadDefaults = false ) {
 51+ return new self( $data, $loadDefaults );
 52+ }
 53+
 54+ /**
 55+ * @see parent::getFieldTypes
 56+ *
 57+ * @since 0.1
 58+ *
 59+ * @return string
 60+ */
 61+ public function getDBTable() {
 62+ return 'contest_challenges';
 63+ }
 64+
 65+ /**
 66+ * @see parent::getFieldTypes
 67+ *
 68+ * @since 0.1
 69+ *
 70+ * @return string
 71+ */
 72+ protected function getFieldPrefix() {
 73+ return 'challenge_';
 74+ }
 75+
 76+ /**
 77+ * @see parent::getFieldTypes
 78+ *
 79+ * @since 0.1
 80+ *
 81+ * @return array
 82+ */
 83+ protected function getFieldTypes() {
 84+ return array(
 85+ 'id' => 'id',
 86+ 'contest_id' => 'id',
 87+ 'text' => 'str',
 88+ 'title' => 'str',
 89+ 'oneline' => 'str',
 90+ );
 91+ }
 92+
 93+ /**
 94+ * @see parent::getDefaults
 95+ *
 96+ * @since 0.1
 97+ *
 98+ * @return array
 99+ */
 100+ public function getDefaults() {
 101+ return array(
 102+ 'text' => '',
 103+ 'title' => '',
 104+ 'oneline' => '',
 105+ );
 106+ }
 107+
 108+ /**
 109+ * Returns an array with challenge IDs (keys) and their associated titles (values)
 110+ * for the provided list of IDs.
 111+ *
 112+ * @param array|integer $ids
 113+ *
 114+ * @return array( id => title )
 115+ */
 116+ public static function getTitlesForIds( $ids ) {
 117+ $challenges = self::s()->select( array( 'id', 'title' ), array( 'id' => $ids ) );
 118+ $results = array();
 119+
 120+ foreach ( $challenges as /* ContestChallenge */ $challenge ) {
 121+ $results[$challenge->getId()] = $challenge->getField( 'title' );
 122+ }
 123+
 124+ return $results;
 125+ }
 126+
 127+}
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/includes/ContestChallenge.php
___________________________________________________________________
Added: svn:eol-style
1128 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/includes/ContestComment.php
@@ -0,0 +1,123 @@
 2+<?php
 3+
 4+/**
 5+ * Class representing a single contest comment.
 6+ * Comments can be made by judges on (submissions of) contestants.
 7+ *
 8+ * @since 0.1
 9+ *
 10+ * @file ContestComment.php
 11+ * @ingroup Contest
 12+ *
 13+ * @licence GNU GPL v3 or later
 14+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
 15+ */
 16+class ContestComment extends ContestDBObject {
 17+
 18+ /**
 19+ * Method to get an instance so methods that ought to be static,
 20+ * but can't be due to PHP 5.2 not having LSB, can be called on
 21+ * it. This also allows easy identifying of code that needs to
 22+ * be changed once PHP 5.3 becomes an acceptable requirement.
 23+ *
 24+ * @since 0.1
 25+ *
 26+ * @return ContestDBObject
 27+ */
 28+ public static function s() {
 29+ static $instance = false;
 30+
 31+ if ( $instance === false ) {
 32+ $instance = new self( array() );
 33+ }
 34+
 35+ return $instance;
 36+ }
 37+
 38+ /**
 39+ * Get a new instance of the class from an array.
 40+ * This method ought to be in the basic class and
 41+ * return a new static(), but this requires LSB/PHP>=5.3.
 42+ *
 43+ * @since 0.1
 44+ *
 45+ * @param array $data
 46+ * @param boolean $loadDefaults
 47+ *
 48+ * @return ContestDBObject
 49+ */
 50+ public function newFromArray( array $data, $loadDefaults = false ) {
 51+ return new self( $data, $loadDefaults );
 52+ }
 53+
 54+ /**
 55+ * @see parent::getFieldTypes
 56+ *
 57+ * @since 0.1
 58+ *
 59+ * @return string
 60+ */
 61+ public function getDBTable() {
 62+ return 'contest_comments';
 63+ }
 64+
 65+ /**
 66+ * @see parent::getFieldTypes
 67+ *
 68+ * @since 0.1
 69+ *
 70+ * @return string
 71+ */
 72+ protected function getFieldPrefix() {
 73+ return 'comment_';
 74+ }
 75+
 76+ /**
 77+ * @see parent::getFieldTypes
 78+ *
 79+ * @since 0.1
 80+ *
 81+ * @return array
 82+ */
 83+ protected function getFieldTypes() {
 84+ return array(
 85+ 'id' => 'id',
 86+ 'contestant_id' => 'id',
 87+ 'user_id' => 'id',
 88+
 89+ 'text' => 'str',
 90+ 'time' => 'int',
 91+ );
 92+ }
 93+
 94+ /**
 95+ * @see parent::getDefaults
 96+ *
 97+ * @since 0.1
 98+ *
 99+ * @return array
 100+ */
 101+ public function getDefaults() {
 102+ return array(
 103+ 'text' => '',
 104+ 'time' => 0,
 105+ );
 106+ }
 107+
 108+ /**
 109+ * (non-PHPdoc)
 110+ * @see ContestDBObject::insertIntoDB()
 111+ * @return bool
 112+ */
 113+ protected function insertIntoDB() {
 114+ $success = parent::insertIntoDB();
 115+
 116+ if ( $success ) {
 117+ $contestant = new ContestContestant( array( 'id' => $this->getField( 'contestant_id' ) ) );
 118+ $contestant->addToField( 'comments', 1 );
 119+ }
 120+
 121+ return $success;
 122+ }
 123+
 124+}
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/includes/ContestComment.php
___________________________________________________________________
Added: svn:eol-style
1125 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/includes/ContestContestant.php
@@ -0,0 +1,602 @@
 2+<?php
 3+
 4+/**
 5+ * Class representing a single contest contestant.
 6+ * A contestant is a unique user + contest combination.
 7+ *
 8+ * @since 0.1
 9+ *
 10+ * @file ContestContestant.php
 11+ * @ingroup Contest
 12+ *
 13+ * @licence GNU GPL v3 or later
 14+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
 15+ */
 16+class ContestContestant extends ContestDBObject {
 17+
 18+ protected $contest = null;
 19+
 20+ /**
 21+ * Cached user object, created from the user_id field.
 22+ *
 23+ * @since 0.1
 24+ * @var USer
 25+ */
 26+ protected $user = null;
 27+
 28+ /**
 29+ * Method to get an instance so methods that ought to be static,
 30+ * but can't be due to PHP 5.2 not having LSB, can be called on
 31+ * it. This also allows easy identifying of code that needs to
 32+ * be changed once PHP 5.3 becomes an acceptable requirement.
 33+ *
 34+ * @since 0.1
 35+ *
 36+ * @return ContestDBObject
 37+ */
 38+ public static function s() {
 39+ static $instance = false;
 40+
 41+ if ( $instance === false ) {
 42+ $instance = new self( array() );
 43+ }
 44+
 45+ return $instance;
 46+ }
 47+
 48+ /**
 49+ * Get a new instance of the class from an array.
 50+ * This method ought to be in the basic class and
 51+ * return a new static(), but this requires LSB/PHP>=5.3.
 52+ *
 53+ * @since 0.1
 54+ *
 55+ * @param array $data
 56+ * @param boolean $loadDefaults
 57+ *
 58+ * @return ContestDBObject
 59+ */
 60+ public function newFromArray( array $data, $loadDefaults = false ) {
 61+ return new self( $data, $loadDefaults );
 62+ }
 63+
 64+ /**
 65+ * @see parent::getFieldTypes
 66+ *
 67+ * @since 0.1
 68+ *
 69+ * @return string
 70+ */
 71+ public function getDBTable() {
 72+ return 'contest_contestants';
 73+ }
 74+
 75+ /**
 76+ * @see parent::getFieldTypes
 77+ *
 78+ * @since 0.1
 79+ *
 80+ * @return string
 81+ */
 82+ protected function getFieldPrefix() {
 83+ return 'contestant_';
 84+ }
 85+
 86+ /**
 87+ * @see parent::getFieldTypes
 88+ *
 89+ * @since 0.1
 90+ *
 91+ * @return array
 92+ */
 93+ protected function getFieldTypes() {
 94+ return array(
 95+ 'id' => 'id',
 96+ 'contest_id' => 'id',
 97+ 'challenge_id' => 'id',
 98+ 'user_id' => 'id',
 99+
 100+ 'full_name' => 'str',
 101+ 'user_name' => 'str',
 102+ 'email' => 'str',
 103+
 104+ 'country' => 'str',
 105+ 'volunteer' => 'bool',
 106+ 'wmf' => 'bool',
 107+ 'cv' => 'str',
 108+
 109+ 'submission' => 'str',
 110+
 111+ 'rating' => 'float',
 112+ 'rating_count' => 'int',
 113+ 'comments' => 'int',
 114+ );
 115+ }
 116+
 117+ /**
 118+ * @see parent::getDefaults
 119+ *
 120+ * @since 0.1
 121+ *
 122+ * @return array
 123+ */
 124+ public function getDefaults() {
 125+ return array(
 126+ 'full_name' => '',
 127+ 'user_name' => '',
 128+ 'email' => '',
 129+
 130+ 'country' => '',
 131+ 'volunteer' => false,
 132+ 'wmf' => false,
 133+ 'cv' => false,
 134+
 135+ 'submission' => '',
 136+
 137+ 'rating' => 0,
 138+ 'rating_count' => 0,
 139+ 'comments' => 0,
 140+ );
 141+ }
 142+
 143+ /**
 144+ * Gets the contest for this participant.
 145+ *
 146+ * @since 0.1
 147+ *
 148+ * @param array|string|null $fields The fields to load, null for all fields.
 149+ *
 150+ * @return Contest
 151+ */
 152+ public function getContest( $fields = null ) {
 153+ if ( !is_null( $this->contest ) ) {
 154+ return $this->contest;
 155+ }
 156+
 157+ $contest = Contest::s()->selectRow( $fields, array( 'id' => $this->getField( 'contest_id' ) ) );
 158+
 159+ if ( is_null( $this->contest ) && is_null( $fields ) ) {
 160+ $this->contest = $contest;
 161+ }
 162+
 163+ return $contest;
 164+ }
 165+
 166+ /**
 167+ * Sets the contest for this participant.
 168+ *
 169+ * @since 0.1
 170+ *
 171+ * @param Contest $contest
 172+ */
 173+ public function setContest( Contest $contest ) {
 174+ $this->contest = $contest;
 175+ }
 176+
 177+ /**
 178+ * Returns a list of countries and their corresponding country
 179+ * codes that can be fed directly into an HTML input.
 180+ *
 181+ * @since 0.1
 182+ *
 183+ * @param booolean $addEmptyItem
 184+ *
 185+ * @return array
 186+ */
 187+ public static function getCountriesForInput( $addEmptyItem = false ) {
 188+ $countries = array();
 189+
 190+ if ( $addEmptyItem ) {
 191+ $countries[''] = '';
 192+ }
 193+
 194+ foreach ( self::getCountries() as $code => $name ) {
 195+ $countries["$code - $name"] = $code;
 196+ }
 197+
 198+ return $countries;
 199+ }
 200+
 201+ /**
 202+ * Returns a list of ISO 3166-1-alpha-2 country codes (keys) and their corresponding country (values).
 203+ *
 204+ * @since 0.1
 205+ *
 206+ * @return array
 207+ */
 208+ public static function getCountries() {
 209+ return array(
 210+ 'AF' => 'Afghanistan',
 211+ 'AL' => 'Albania',
 212+ 'DZ' => 'Algeria',
 213+ 'AS' => 'American Samoa',
 214+ 'AD' => 'Andorra',
 215+ 'AO' => 'Angola',
 216+ 'AI' => 'Anguilla',
 217+ 'AQ' => 'Antarctica',
 218+ 'AG' => 'Antigua and Barbuda',
 219+ 'AR' => 'Argentina',
 220+ 'AM' => 'Armenia',
 221+ 'AW' => 'Aruba',
 222+ 'AU' => 'Australia',
 223+ 'AT' => 'Austria',
 224+ 'AZ' => 'Azerbaijan',
 225+ 'BS' => 'Bahamas',
 226+ 'BH' => 'Bahrain',
 227+ 'BD' => 'Bangladesh',
 228+ 'BB' => 'Barbados',
 229+ 'BY' => 'Belarus',
 230+ 'BE' => 'Belgium',
 231+ 'BZ' => 'Belize',
 232+ 'BJ' => 'Benin',
 233+ 'BM' => 'Bermuda',
 234+ 'BT' => 'Bhutan',
 235+ 'BO' => 'Bolivia',
 236+ 'BA' => 'Bosnia and Herzegovina',
 237+ 'BW' => 'Botswana',
 238+ 'BV' => 'Bouvet Island',
 239+ 'BR' => 'Brazil',
 240+ 'IO' => 'British Indian Ocean Territory',
 241+ 'BN' => 'Brunei Darussalam',
 242+ 'BG' => 'Bulgaria',
 243+ 'BF' => 'Burkina Faso',
 244+ 'BI' => 'Burundi',
 245+ 'KH' => 'Cambodia',
 246+ 'CM' => 'Cameroon',
 247+ 'CA' => 'Canada',
 248+ 'CV' => 'Cape Verde',
 249+ 'KY' => 'Cayman Islands',
 250+ 'CF' => 'Central African Republic',
 251+ 'TD' => 'Chad',
 252+ 'CL' => 'Chile',
 253+ 'CN' => 'China',
 254+ 'CX' => 'Christmas Island',
 255+ 'CC' => 'Cocos (Keeling) Islands',
 256+ 'CO' => 'Colombia',
 257+ 'KM' => 'Comoros',
 258+ 'CG' => 'Congo',
 259+ 'CD' => 'Congo, the Democratic Republic of the',
 260+ 'CK' => 'Cook Islands',
 261+ 'CR' => 'Costa Rica',
 262+ 'CI' => "Cote D'Ivoire",
 263+ 'HR' => 'Croatia',
 264+ 'CU' => 'Cuba',
 265+ 'CY' => 'Cyprus',
 266+ 'CZ' => 'Czech Republic',
 267+ 'DK' => 'Denmark',
 268+ 'DJ' => 'Djibouti',
 269+ 'DM' => 'Dominica',
 270+ 'DO' => 'Dominican Republic',
 271+ 'EC' => 'Ecuador',
 272+ 'EG' => 'Egypt',
 273+ 'SV' => 'El Salvador',
 274+ 'GQ' => 'Equatorial Guinea',
 275+ 'ER' => 'Eritrea',
 276+ 'EE' => 'Estonia',
 277+ 'ET' => 'Ethiopia',
 278+ 'FK' => 'Falkland Islands (Malvinas)',
 279+ 'FO' => 'Faroe Islands',
 280+ 'FJ' => 'Fiji',
 281+ 'FI' => 'Finland',
 282+ 'FR' => 'France',
 283+ 'GF' => 'French Guiana',
 284+ 'PF' => 'French Polynesia',
 285+ 'TF' => 'French Southern Territories',
 286+ 'GA' => 'Gabon',
 287+ 'GM' => 'Gambia',
 288+ 'GE' => 'Georgia',
 289+ 'DE' => 'Germany',
 290+ 'GH' => 'Ghana',
 291+ 'GI' => 'Gibraltar',
 292+ 'GR' => 'Greece',
 293+ 'GL' => 'Greenland',
 294+ 'GD' => 'Grenada',
 295+ 'GP' => 'Guadeloupe',
 296+ 'GU' => 'Guam',
 297+ 'GT' => 'Guatemala',
 298+ 'GN' => 'Guinea',
 299+ 'GW' => 'Guinea-Bissau',
 300+ 'GY' => 'Guyana',
 301+ 'HT' => 'Haiti',
 302+ 'HM' => 'Heard Island and Mcdonald Islands',
 303+ 'VA' => 'Holy See (Vatican City State)',
 304+ 'HN' => 'Honduras',
 305+ 'HK' => 'Hong Kong',
 306+ 'HU' => 'Hungary',
 307+ 'IS' => 'Iceland',
 308+ 'IN' => 'India',
 309+ 'ID' => 'Indonesia',
 310+ 'IR' => 'Iran, Islamic Republic of',
 311+ 'IQ' => 'Iraq',
 312+ 'IE' => 'Ireland',
 313+ 'IL' => 'Israel',
 314+ 'IT' => 'Italy',
 315+ 'JM' => 'Jamaica',
 316+ 'JP' => 'Japan',
 317+ 'JO' => 'Jordan',
 318+ 'KZ' => 'Kazakhstan',
 319+ 'KE' => 'Kenya',
 320+ 'KI' => 'Kiribati',
 321+ 'KP' => "Korea, Democratic People's Republic of",
 322+ 'KR' => 'Korea, Republic of',
 323+ 'KW' => 'Kuwait',
 324+ 'KG' => 'Kyrgyzstan',
 325+ 'LA' => "Lao People's Democratic Republic",
 326+ 'LV' => 'Latvia',
 327+ 'LB' => 'Lebanon',
 328+ 'LS' => 'Lesotho',
 329+ 'LR' => 'Liberia',
 330+ 'LY' => 'Libyan Arab Jamahiriya',
 331+ 'LI' => 'Liechtenstein',
 332+ 'LT' => 'Lithuania',
 333+ 'LU' => 'Luxembourg',
 334+ 'MO' => 'Macao',
 335+ 'MK' => 'Macedonia, the Former Yugoslav Republic of',
 336+ 'MG' => 'Madagascar',
 337+ 'MW' => 'Malawi',
 338+ 'MY' => 'Malaysia',
 339+ 'MV' => 'Maldives',
 340+ 'ML' => 'Mali',
 341+ 'MT' => 'Malta',
 342+ 'MH' => 'Marshall Islands',
 343+ 'MQ' => 'Martinique',
 344+ 'MR' => 'Mauritania',
 345+ 'MU' => 'Mauritius',
 346+ 'YT' => 'Mayotte',
 347+ 'MX' => 'Mexico',
 348+ 'FM' => 'Micronesia, Federated States of',
 349+ 'MD' => 'Moldova, Republic of',
 350+ 'MC' => 'Monaco',
 351+ 'MN' => 'Mongolia',
 352+ 'MS' => 'Montserrat',
 353+ 'MA' => 'Morocco',
 354+ 'MZ' => 'Mozambique',
 355+ 'MM' => 'Myanmar',
 356+ 'NA' => 'Namibia',
 357+ 'NR' => 'Nauru',
 358+ 'NP' => 'Nepal',
 359+ 'NL' => 'Netherlands',
 360+ 'AN' => 'Netherlands Antilles',
 361+ 'NC' => 'New Caledonia',
 362+ 'NZ' => 'New Zealand',
 363+ 'NI' => 'Nicaragua',
 364+ 'NE' => 'Niger',
 365+ 'NG' => 'Nigeria',
 366+ 'NU' => 'Niue',
 367+ 'NF' => 'Norfolk Island',
 368+ 'MP' => 'Northern Mariana Islands',
 369+ 'NO' => 'Norway',
 370+ 'OM' => 'Oman',
 371+ 'PK' => 'Pakistan',
 372+ 'PW' => 'Palau',
 373+ 'PS' => 'Palestinian Territory, Occupied',
 374+ 'PA' => 'Panama',
 375+ 'PG' => 'Papua New Guinea',
 376+ 'PY' => 'Paraguay',
 377+ 'PE' => 'Peru',
 378+ 'PH' => 'Philippines',
 379+ 'PN' => 'Pitcairn',
 380+ 'PL' => 'Poland',
 381+ 'PT' => 'Portugal',
 382+ 'PR' => 'Puerto Rico',
 383+ 'QA' => 'Qatar',
 384+ 'RE' => 'Reunion',
 385+ 'RO' => 'Romania',
 386+ 'RU' => 'Russian Federation',
 387+ 'RW' => 'Rwanda',
 388+ 'SH' => 'Saint Helena',
 389+ 'KN' => 'Saint Kitts and Nevis',
 390+ 'LC' => 'Saint Lucia',
 391+ 'PM' => 'Saint Pierre and Miquelon',
 392+ 'VC' => 'Saint Vincent and the Grenadines',
 393+ 'WS' => 'Samoa',
 394+ 'SM' => 'San Marino',
 395+ 'ST' => 'Sao Tome and Principe',
 396+ 'SA' => 'Saudi Arabia',
 397+ 'SN' => 'Senegal',
 398+ 'CS' => 'Serbia and Montenegro',
 399+ 'SC' => 'Seychelles',
 400+ 'SL' => 'Sierra Leone',
 401+ 'SG' => 'Singapore',
 402+ 'SK' => 'Slovakia',
 403+ 'SI' => 'Slovenia',
 404+ 'SB' => 'Solomon Islands',
 405+ 'SO' => 'Somalia',
 406+ 'ZA' => 'South Africa',
 407+ //'GS' => 'South Georgia and the South Sandwich Islands',
 408+ 'ES' => 'Spain',
 409+ 'LK' => 'Sri Lanka',
 410+ 'SD' => 'Sudan',
 411+ 'SR' => 'Suriname',
 412+ 'SJ' => 'Svalbard and Jan Mayen',
 413+ 'SZ' => 'Swaziland',
 414+ 'SE' => 'Sweden',
 415+ 'CH' => 'Switzerland',
 416+ 'SY' => 'Syrian Arab Republic',
 417+ 'TW' => 'Taiwan, Province of China',
 418+ 'TJ' => 'Tajikistan',
 419+ 'TZ' => 'Tanzania, United Republic of',
 420+ 'TH' => 'Thailand',
 421+ 'TL' => 'Timor-Leste',
 422+ 'TG' => 'Togo',
 423+ 'TK' => 'Tokelau',
 424+ 'TO' => 'Tonga',
 425+ 'TT' => 'Trinidad and Tobago',
 426+ 'TN' => 'Tunisia',
 427+ 'TR' => 'Turkey',
 428+ 'TM' => 'Turkmenistan',
 429+ 'TC' => 'Turks and Caicos Islands',
 430+ 'TV' => 'Tuvalu',
 431+ 'UG' => 'Uganda',
 432+ 'UA' => 'Ukraine',
 433+ 'AE' => 'United Arab Emirates',
 434+ 'GB' => 'United Kingdom',
 435+ 'US' => 'United States',
 436+ 'UM' => 'United States Minor Outlying Islands',
 437+ 'UY' => 'Uruguay',
 438+ 'UZ' => 'Uzbekistan',
 439+ 'VU' => 'Vanuatu',
 440+ 'VE' => 'Venezuela',
 441+ 'VN' => 'Viet Nam',
 442+ 'VG' => 'Virgin Islands, British',
 443+ 'VI' => 'Virgin Islands, U.s.',
 444+ 'WF' => 'Wallis and Futuna',
 445+ 'EH' => 'Western Sahara',
 446+ 'YE' => 'Yemen',
 447+ 'ZM' => 'Zambia',
 448+ 'ZW' => 'Zimbabwe'
 449+ );
 450+ }
 451+
 452+ /**
 453+ * (non-PHPdoc)
 454+ * @see ContestDBObject::insertIntoDB()
 455+ * @return bool
 456+ */
 457+ protected function insertIntoDB() {
 458+ wfRunHooks( 'ContestBeforeContestantInsert', array( &$this ) );
 459+
 460+ $success = parent::insertIntoDB();
 461+
 462+ if ( $success ) {
 463+ $this->onUserSignup();
 464+ }
 465+
 466+ return $success;
 467+ }
 468+
 469+ /**
 470+ * Handles successfull user signup for a contest.
 471+ *
 472+ * @since 0.1
 473+ */
 474+ protected function onUserSignup() {
 475+ $this->getContest( array( 'id' ) )->addToSubmissionCount( 1 );
 476+
 477+ $this->getUser()->setOption( 'contest_showtoplink', true );
 478+ $this->getUser()->saveSettings(); // TODO: can't we just save this single option instead of everything?
 479+
 480+ $this->sendSignupEmail();
 481+
 482+ wfRunHooks( 'ContestAfterContestantInsert', array( &$this ) );
 483+ }
 484+
 485+ /**
 486+ * Send the signup email.
 487+ *
 488+ * @since 0.1
 489+ *
 490+ * @return Status
 491+ */
 492+ public function sendSignupEmail() {
 493+ global $wgPasswordSender, $wgPasswordSenderName;
 494+
 495+ $title = wfMsg( 'contest-email-signup-title' );
 496+ $emailText = ContestUtils::getParsedArticleContent( $this->getContest()->getField( 'signup_email' ) );
 497+ $user = $this->getUser();
 498+ $sender = $wgPasswordSender;
 499+ $senderName = $wgPasswordSenderName;
 500+
 501+ wfRunHooks( 'ContestBeforeSignupEmail', array( &$this, &$title, &$emailText, &$user, &$sender, &$senderName ) );
 502+
 503+ return UserMailer::send(
 504+ new MailAddress( $user ),
 505+ new MailAddress( $sender, $senderName ),
 506+ $title,
 507+ $emailText,
 508+ null,
 509+ 'text/html; charset=ISO-8859-1'
 510+ );
 511+ }
 512+
 513+ /**
 514+ * Send a reminder email.
 515+ *
 516+ * @since 0.1
 517+ *
 518+ * @return Status
 519+ */
 520+ public function sendReminderEmail( $emailText, array $params = array() ) {
 521+ global $wgPasswordSender, $wgPasswordSenderName;
 522+
 523+ if ( !array_key_exists( 'daysLeft', $params ) ) {
 524+ $params['daysLeft'] = $this->getContest()->getDaysLeft();
 525+ }
 526+
 527+ $title = wfMsgExt( 'contest-email-reminder-title', 'parsemag', $params['daysLeft'] );
 528+ $user = $this->getUser();
 529+ $sender = $wgPasswordSender;
 530+ $senderName = $wgPasswordSenderName;
 531+
 532+ wfRunHooks( 'ContestBeforeReminderEmail', array( &$this, &$title, &$emailText, &$user, &$sender, &$senderName ) );
 533+
 534+ return UserMailer::send(
 535+ new MailAddress( $user ),
 536+ new MailAddress( $sender, $senderName ),
 537+ $title,
 538+ $emailText,
 539+ null,
 540+ 'text/html; charset=ISO-8859-1'
 541+ );
 542+ }
 543+
 544+ /**
 545+ * Update the vote count and avarage vote fields.
 546+ * This does not write the changes to the database,
 547+ * if this is required, call writeToDB.
 548+ *
 549+ * @since 0.1
 550+ */
 551+ public function updateVotes() {
 552+ $votes = $this->getVotes();
 553+
 554+ $amount = count( $votes );
 555+ $total = 0;
 556+
 557+ foreach ( $votes as /* ContestVote */ $vote ) {
 558+ $total += $vote->getField( 'value' );
 559+ }
 560+
 561+ $this->setField( 'rating_count', $amount );
 562+ $this->setField( 'rating', $amount > 0 ? $total / $amount : 0 );
 563+ }
 564+
 565+ /**
 566+ * Returns the user object for this contestant, created
 567+ * from the user_id field and cached in $this->user.
 568+ *
 569+ * @since 0.1
 570+ *
 571+ * @return User
 572+ */
 573+ public function getUser() {
 574+ if ( is_null( $this->user ) ) {
 575+ $this->user = User::newFromId( $this->getField( 'user_id' ) );
 576+ }
 577+
 578+ return $this->user;
 579+ }
 580+
 581+ /**
 582+ * Get the votes for this contestant.
 583+ *
 584+ * @since 0.1
 585+ *
 586+ * @return array of ContestVote
 587+ */
 588+ public function getVotes() {
 589+ return ContestVote::s()->select( null, array( 'contestant_id' => $this->getId() ) );
 590+ }
 591+
 592+ /**
 593+ * Get the comments for this contestant.
 594+ *
 595+ * @since 0.1
 596+ *
 597+ * @return array of ContestComment
 598+ */
 599+ public function getComments() {
 600+ return ContestComment::s()->select( null, array( 'contestant_id' => $this->getId() ) );
 601+ }
 602+
 603+}
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/includes/ContestContestant.php
___________________________________________________________________
Added: svn:eol-style
1604 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/includes/Contest.class.php
@@ -0,0 +1,483 @@
 2+<?php
 3+
 4+/**
 5+ * Class representing a single contest.
 6+ *
 7+ * @since 0.1
 8+ *
 9+ * @file Contest.class.php
 10+ * @ingroup Contest
 11+ *
 12+ * @licence GNU GPL v3 or later
 13+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
 14+ */
 15+class Contest extends ContestDBObject {
 16+
 17+ // Constants representing the states a contest can have.
 18+ const STATUS_DRAFT = 0;
 19+ const STATUS_ACTIVE = 1;
 20+ const STATUS_FINISHED = 2; // manually stopped by contest manager
 21+ const STATUS_EXPIRED = 3; // past configured contest end date
 22+
 23+ /**
 24+ * List of challenges for this contest.
 25+ * @see loadChallenges, setChallenges and writeChallengesToDB
 26+ *
 27+ * @since 0.1
 28+ * @var array of ContestChallenge
 29+ */
 30+ protected $challenges = null;
 31+
 32+ /**
 33+ * List of contestants for this contest.
 34+ * @see loadContestants, setContestants and writeContestantsToDB
 35+ *
 36+ * @since 0.1
 37+ * @var array of ContestContestant
 38+ */
 39+ protected $contestants = null;
 40+
 41+ /**
 42+ * Indicates if the contest was set from non-finished to finished.
 43+ * This is used to take further action on save of the object.
 44+ *
 45+ * @since 0.1
 46+ * @var boolean
 47+ */
 48+ protected $wasSetToFinished = false;
 49+
 50+ /**
 51+ * Method to get an instance so methods that ought to be static,
 52+ * but can't be due to PHP 5.2 not having LSB, can be called on
 53+ * it. This also allows easy identifying of code that needs to
 54+ * be changed once PHP 5.3 becomes an acceptable requirement.
 55+ *
 56+ * @since 0.1
 57+ *
 58+ * @return ContestDBObject
 59+ */
 60+ public static function s() {
 61+ static $instance = false;
 62+
 63+ if ( $instance === false ) {
 64+ $instance = new self( array() );
 65+ }
 66+
 67+ return $instance;
 68+ }
 69+
 70+ /**
 71+ * Get a new instance of the class from an array.
 72+ * This method ought to be in the basic class and
 73+ * return a new static(), but this requires LSB/PHP>=5.3.
 74+ *
 75+ * @since 0.1
 76+ *
 77+ * @param array $data
 78+ * @param boolean $loadDefaults
 79+ *
 80+ * @return ContestDBObject
 81+ */
 82+ public function newFromArray( array $data, $loadDefaults = false ) {
 83+ return new self( $data, $loadDefaults );
 84+ }
 85+
 86+ /**
 87+ * @see parent::getFieldTypes
 88+ *
 89+ * @since 0.1
 90+ *
 91+ * @return string
 92+ */
 93+ public function getDBTable() {
 94+ return 'contests';
 95+ }
 96+
 97+ /**
 98+ * @see parent::getFieldTypes
 99+ *
 100+ * @since 0.1
 101+ *
 102+ * @return string
 103+ */
 104+ protected function getFieldPrefix() {
 105+ return 'contest_';
 106+ }
 107+
 108+ /**
 109+ * @see parent::getFieldTypes
 110+ *
 111+ * @since 0.1
 112+ *
 113+ * @return array
 114+ */
 115+ protected function getFieldTypes() {
 116+ return array(
 117+ 'id' => 'id',
 118+ 'name' => 'str',
 119+ 'status' => 'int',
 120+ 'end' => 'str', // TS_MW
 121+
 122+ 'rules_page' => 'str',
 123+ 'opportunities' => 'str',
 124+ 'intro' => 'str',
 125+ 'help' => 'str',
 126+ 'signup_email' => 'str',
 127+ 'reminder_email' => 'str',
 128+
 129+ 'submission_count' => 'int',
 130+ );
 131+ }
 132+
 133+ /**
 134+ * @see parent::getDefaults
 135+ *
 136+ * @since 0.1
 137+ *
 138+ * @return array
 139+ */
 140+ public function getDefaults() {
 141+ return array(
 142+ 'name' => '',
 143+ 'status' => self::STATUS_DRAFT,
 144+ 'end' => '',
 145+
 146+ 'rules_page' => 'MediaWiki:Contests/',
 147+ 'opportunities' => 'MediaWiki:Contests/',
 148+ 'intro' => 'MediaWiki:Contests/',
 149+ 'help' => '',
 150+ 'signup_email' => 'MediaWiki:Contests/',
 151+ 'reminder_email' => 'MediaWiki:Contests/',
 152+
 153+ 'submission_count' => 0,
 154+ );
 155+ }
 156+
 157+ /**
 158+ * Gets the message for the provided status.
 159+ *
 160+ * @param Contest::STATUS_ $status
 161+ *
 162+ * @return string
 163+ */
 164+ public static function getStatusMessage( $status ) {
 165+ static $map = false;
 166+
 167+ if ( $map === false ) {
 168+ $map = array_flip( self::getStatusMessages() );
 169+ }
 170+
 171+ return $map[$status];
 172+ }
 173+
 174+ /**
 175+ * Returns a list of status messages and their corresponding constants.
 176+ *
 177+ * @param boolean $onlySettable Whether to return only messages for modifiable status.
 178+ *
 179+ * @since 0.1
 180+ *
 181+ * @return array
 182+ */
 183+ public static function getStatusMessages( $onlySettable = false ) {
 184+ static $map = false;
 185+
 186+ if ( $map === false ) {
 187+ $map = array(
 188+ wfMsg( 'contest-status-draft' ) => self::STATUS_DRAFT,
 189+ wfMsg( 'contest-status-active' ) => self::STATUS_ACTIVE,
 190+ wfMsg( 'contest-status-finished' ) => self::STATUS_FINISHED,
 191+ );
 192+ }
 193+
 194+ if ( !$onlySettable ) {
 195+ $map[wfMsg( 'contest-status-expired')] = self::STATUS_EXPIRED;
 196+ }
 197+
 198+ return $map;
 199+ }
 200+
 201+ /**
 202+ * Load the challenges from the database.
 203+ * Any set challenges will be lost.
 204+ *
 205+ * @since 0.1
 206+ */
 207+ public function loadChallenges() {
 208+ $this->challenges = ContestChallenge::s()->select(
 209+ null,
 210+ array( 'contest_id' => $this->getId() )
 211+ );
 212+ }
 213+
 214+ /**
 215+ * Gets the challenges that are part of this contest.
 216+ *
 217+ * @since 0.1
 218+ *
 219+ * @return array of ContestChallenge
 220+ */
 221+ public function getChallenges( $forceLoad = false ) {
 222+ if ( is_null( $this->challenges ) || $forceLoad ) {
 223+ $this->loadChallenges();
 224+ }
 225+
 226+ return $this->challenges;
 227+ }
 228+
 229+ /**
 230+ * Load the contestants from the database.
 231+ * Any set contestants will be lost.
 232+ *
 233+ * @since 0.1
 234+ */
 235+ public function loadContestants() {
 236+ $this->contestants = ContestContestant::s()->select(
 237+ null,
 238+ array( 'contest_id' => $this->getId() )
 239+ );
 240+ }
 241+
 242+ /**
 243+ * Gets the contestants for this contest.
 244+ *
 245+ * @since 0.1
 246+ *
 247+ * @return array of ContestContestant
 248+ */
 249+ public function getContestants( $forceLoad = false ) {
 250+ if ( is_null( $this->contestants ) || $forceLoad ) {
 251+ $this->loadContestants();
 252+ }
 253+
 254+ return $this->contestants;
 255+ }
 256+
 257+ /**
 258+ * Set the contestants for this contest.
 259+ *
 260+ * @since 0.1
 261+ *
 262+ * @param array $contestants
 263+ */
 264+ public function setContestants( array /* of ContestContestant */ $contestants ) {
 265+ $this->contestants = $contestants;
 266+ }
 267+
 268+ /**
 269+ * Set the challenges for this contest.
 270+ *
 271+ * @since 0.1
 272+ *
 273+ * @param array $challenges
 274+ */
 275+ public function setChallenges( array /* of ContestChallenge */ $challenges ) {
 276+ $this->challenges = $challenges;
 277+ }
 278+
 279+ /**
 280+ * (non-PHPdoc)
 281+ * @see ContestDBObject::writeToDB()
 282+ * @return bool
 283+ */
 284+ public function writeToDB() {
 285+ $success = parent::writeToDB();
 286+
 287+ if ( $success && $this->wasSetToFinished ) {
 288+ $this->doFinishActions();
 289+ $this->wasSetToFinished = false;
 290+ }
 291+
 292+ return $success;
 293+ }
 294+
 295+ /**
 296+ * Write the contest and all set challenges and participants to the database.
 297+ *
 298+ * @since 0.1
 299+ *
 300+ * @return boolean Success indicator
 301+ */
 302+ public function writeAllToDB() {
 303+ $success = self::writeToDB();
 304+
 305+ if ( $success ) {
 306+ $success = $this->writeChallengesToDB();
 307+ }
 308+
 309+ if ( $success ) {
 310+ $success = $this->writeContestantsToDB();
 311+ }
 312+
 313+ return $success;
 314+ }
 315+
 316+ /**
 317+ * Write the challenges to the database.
 318+ *
 319+ * @since 0.1
 320+ *
 321+ * @return boolean Success indicator
 322+ */
 323+ public function writeChallengesToDB() {
 324+ if ( is_null( $this->challenges ) || count( $this->challenges ) == 0 ) {
 325+ return true;
 326+ }
 327+
 328+ $dbw = wfGetDB( DB_MASTER );
 329+ $success = true;
 330+
 331+ $dbw->begin();
 332+
 333+ foreach ( $this->challenges as /* ContestChallenge */ $challenge ) {
 334+ $challenge->setField( 'contest_id', $this->getId() );
 335+ $success &= $challenge->writeToDB();
 336+ }
 337+
 338+ $dbw->commit();
 339+
 340+ return $success;
 341+ }
 342+
 343+ /**
 344+ * Write the contestants to the database.
 345+ *
 346+ * @since 0.1
 347+ *
 348+ * @return boolean Success indicator
 349+ */
 350+ public function writeContestantsToDB() {
 351+ if ( is_null( $this->contestants ) || count( $this->contestants ) == 0 ) {
 352+ return true;
 353+ }
 354+
 355+ $dbw = wfGetDB( DB_MASTER );
 356+ $success = true;
 357+ $nr = 0;
 358+
 359+ $dbw->begin();
 360+
 361+ foreach ( $this->contestants as /* ContestContestant */ $contestant ) {
 362+ $contestant->setField( 'contest_id', $this->getId() );
 363+ $success &= $contestant->writeToDB();
 364+
 365+ if ( ++$nr % 500 == 0 ) {
 366+ $dbw->commit();
 367+ $dbw->begin();
 368+ }
 369+ }
 370+
 371+ $dbw->commit();
 372+
 373+ return $success;
 374+ }
 375+
 376+ /**
 377+ * Add an amount (can be negative) to the total submissions for this contest.
 378+ *
 379+ * @since 0.1
 380+ *
 381+ * @param integer $amount
 382+ *
 383+ * @return boolean Success indicator
 384+ */
 385+ public function addToSubmissionCount( $amount ) {
 386+ return parent::addToField( 'submission_count', $amount );
 387+ }
 388+
 389+ /**
 390+ * (non-PHPdoc)
 391+ * @see ContestDBObject::setField()
 392+ */
 393+ public function setField( $name, $value ) {
 394+ if ( $name == 'status' && $value == self::STATUS_FINISHED
 395+ && $this->hasField( $name ) && $this->getField( $name ) != self::STATUS_FINISHED ) {
 396+ $this->wasSetToFinished = true;
 397+ }
 398+
 399+ parent::setField( $name, $value );
 400+ }
 401+
 402+ /**
 403+ * Remove the contest and all it's linked data from the database.
 404+ *
 405+ * @since 0.1
 406+ *
 407+ * @return boolean Success indicator
 408+ */
 409+ public function removeAllFromDB() {
 410+ $condition = array( 'contest_id' => $this->getId() );
 411+
 412+ $success = ContestChallenge::s()->delete( $condition );
 413+
 414+ if ( $success ) {
 415+ $contestantIds = array();
 416+
 417+ foreach ( ContestContestant::s()->select( 'id', $condition ) as /* ContestContestant */ $contestant ) {
 418+ $contestantIds[] = $contestant->getId();
 419+ }
 420+
 421+ if ( count( $contestantIds ) > 0 ) {
 422+ $success = ContestComment::s()->delete( array( 'contestant_id' => $contestantIds ) ) && $success;
 423+ $success = ContestVote::s()->delete( array( 'contestant_id' => $contestantIds ) ) && $success;
 424+ }
 425+
 426+ $success = ContestContestant::s()->delete( $condition ) && $success;
 427+ }
 428+
 429+ if ( $success ) {
 430+ $success = parent::removeFromDB();
 431+ }
 432+
 433+ return $success;
 434+ }
 435+
 436+ /**
 437+ * Do all actions that need to be done on contest finish.
 438+ *
 439+ * @since 0.1
 440+ */
 441+ public function doFinishActions() {
 442+ // TODO
 443+ }
 444+
 445+ /**
 446+ * Gets the amount of time left, in seconds.
 447+ *
 448+ * @since 0.1
 449+ *
 450+ * @return integer
 451+ */
 452+ public function getTimeLeft() {
 453+ return wfTimestamp( TS_UNIX, $this->getField( 'end' ) ) - time();
 454+ }
 455+
 456+ /**
 457+ * Gets the amount of days left, rounded up to the nearest integer.
 458+ *
 459+ * @since 0.1
 460+ *
 461+ * @return integer
 462+ */
 463+ public function getDaysLeft() {
 464+ return (int)ceil( $this->getTimeLeft() / ( 60 * 60 * 24 ) );
 465+ }
 466+
 467+ /**
 468+ * Gets the contest status, which is either expired, or whatever the
 469+ * contest administrator has manually set it to. Only active contests will
 470+ * be evaluated for expiry.
 471+ *
 472+ * @return integer status constant
 473+ *
 474+ **/
 475+ public function getStatus() {
 476+ $dbStatus = $this->getField( 'status' );
 477+
 478+ if ( $dbStatus === self::STATUS_ACTIVE && $this->getTimeLeft() <= 0 ) {
 479+ return self::STATUS_EXPIRED;
 480+ } else {
 481+ return $dbStatus;
 482+ }
 483+ }
 484+}
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/includes/Contest.class.php
___________________________________________________________________
Added: svn:eol-style
1485 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/COPYING
@@ -0,0 +1,682 @@
 2+The license text below "----" applies to all files within this distribution, other
 3+than those that are in a directory which contains files named "LICENSE" or
 4+"COPYING", or a subdirectory thereof. For those files, the license text contained in
 5+said file overrides any license information contained in directories of smaller depth.
 6+Alternative licenses are typically used for software that is provided by external
 7+parties, and merely packaged with this software for convenience.
 8+----
 9+
 10+ GNU GENERAL PUBLIC LICENSE
 11+ Version 3, 29 June 2007
 12+
 13+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
 14+ Everyone is permitted to copy and distribute verbatim copies
 15+ of this license document, but changing it is not allowed.
 16+
 17+ Preamble
 18+
 19+ The GNU General Public License is a free, copyleft license for
 20+software and other kinds of works.
 21+
 22+ The licenses for most software and other practical works are designed
 23+to take away your freedom to share and change the works. By contrast,
 24+the GNU General Public License is intended to guarantee your freedom to
 25+share and change all versions of a program--to make sure it remains free
 26+software for all its users. We, the Free Software Foundation, use the
 27+GNU General Public License for most of our software; it applies also to
 28+any other work released this way by its authors. You can apply it to
 29+your programs, too.
 30+
 31+ When we speak of free software, we are referring to freedom, not
 32+price. Our General Public Licenses are designed to make sure that you
 33+have the freedom to distribute copies of free software (and charge for
 34+them if you wish), that you receive source code or can get it if you
 35+want it, that you can change the software or use pieces of it in new
 36+free programs, and that you know you can do these things.
 37+
 38+ To protect your rights, we need to prevent others from denying you
 39+these rights or asking you to surrender the rights. Therefore, you have
 40+certain responsibilities if you distribute copies of the software, or if
 41+you modify it: responsibilities to respect the freedom of others.
 42+
 43+ For example, if you distribute copies of such a program, whether
 44+gratis or for a fee, you must pass on to the recipients the same
 45+freedoms that you received. You must make sure that they, too, receive
 46+or can get the source code. And you must show them these terms so they
 47+know their rights.
 48+
 49+ Developers that use the GNU GPL protect your rights with two steps:
 50+(1) assert copyright on the software, and (2) offer you this License
 51+giving you legal permission to copy, distribute and/or modify it.
 52+
 53+ For the developers' and authors' protection, the GPL clearly explains
 54+that there is no warranty for this free software. For both users' and
 55+authors' sake, the GPL requires that modified versions be marked as
 56+changed, so that their problems will not be attributed erroneously to
 57+authors of previous versions.
 58+
 59+ Some devices are designed to deny users access to install or run
 60+modified versions of the software inside them, although the manufacturer
 61+can do so. This is fundamentally incompatible with the aim of
 62+protecting users' freedom to change the software. The systematic
 63+pattern of such abuse occurs in the area of products for individuals to
 64+use, which is precisely where it is most unacceptable. Therefore, we
 65+have designed this version of the GPL to prohibit the practice for those
 66+products. If such problems arise substantially in other domains, we
 67+stand ready to extend this provision to those domains in future versions
 68+of the GPL, as needed to protect the freedom of users.
 69+
 70+ Finally, every program is threatened constantly by software patents.
 71+States should not allow patents to restrict development and use of
 72+software on general-purpose computers, but in those that do, we wish to
 73+avoid the special danger that patents applied to a free program could
 74+make it effectively proprietary. To prevent this, the GPL assures that
 75+patents cannot be used to render the program non-free.
 76+
 77+ The precise terms and conditions for copying, distribution and
 78+modification follow.
 79+
 80+ TERMS AND CONDITIONS
 81+
 82+ 0. Definitions.
 83+
 84+ "This License" refers to version 3 of the GNU General Public License.
 85+
 86+ "Copyright" also means copyright-like laws that apply to other kinds of
 87+works, such as semiconductor masks.
 88+
 89+ "The Program" refers to any copyrightable work licensed under this
 90+License. Each licensee is addressed as "you". "Licensees" and
 91+"recipients" may be individuals or organizations.
 92+
 93+ To "modify" a work means to copy from or adapt all or part of the work
 94+in a fashion requiring copyright permission, other than the making of an
 95+exact copy. The resulting work is called a "modified version" of the
 96+earlier work or a work "based on" the earlier work.
 97+
 98+ A "covered work" means either the unmodified Program or a work based
 99+on the Program.
 100+
 101+ To "propagate" a work means to do anything with it that, without
 102+permission, would make you directly or secondarily liable for
 103+infringement under applicable copyright law, except executing it on a
 104+computer or modifying a private copy. Propagation includes copying,
 105+distribution (with or without modification), making available to the
 106+public, and in some countries other activities as well.
 107+
 108+ To "convey" a work means any kind of propagation that enables other
 109+parties to make or receive copies. Mere interaction with a user through
 110+a computer network, with no transfer of a copy, is not conveying.
 111+
 112+ An interactive user interface displays "Appropriate Legal Notices"
 113+to the extent that it includes a convenient and prominently visible
 114+feature that (1) displays an appropriate copyright notice, and (2)
 115+tells the user that there is no warranty for the work (except to the
 116+extent that warranties are provided), that licensees may convey the
 117+work under this License, and how to view a copy of this License. If
 118+the interface presents a list of user commands or options, such as a
 119+menu, a prominent item in the list meets this criterion.
 120+
 121+ 1. Source Code.
 122+
 123+ The "source code" for a work means the preferred form of the work
 124+for making modifications to it. "Object code" means any non-source
 125+form of a work.
 126+
 127+ A "Standard Interface" means an interface that either is an official
 128+standard defined by a recognized standards body, or, in the case of
 129+interfaces specified for a particular programming language, one that
 130+is widely used among developers working in that language.
 131+
 132+ The "System Libraries" of an executable work include anything, other
 133+than the work as a whole, that (a) is included in the normal form of
 134+packaging a Major Component, but which is not part of that Major
 135+Component, and (b) serves only to enable use of the work with that
 136+Major Component, or to implement a Standard Interface for which an
 137+implementation is available to the public in source code form. A
 138+"Major Component", in this context, means a major essential component
 139+(kernel, window system, and so on) of the specific operating system
 140+(if any) on which the executable work runs, or a compiler used to
 141+produce the work, or an object code interpreter used to run it.
 142+
 143+ The "Corresponding Source" for a work in object code form means all
 144+the source code needed to generate, install, and (for an executable
 145+work) run the object code and to modify the work, including scripts to
 146+control those activities. However, it does not include the work's
 147+System Libraries, or general-purpose tools or generally available free
 148+programs which are used unmodified in performing those activities but
 149+which are not part of the work. For example, Corresponding Source
 150+includes interface definition files associated with source files for
 151+the work, and the source code for shared libraries and dynamically
 152+linked subprograms that the work is specifically designed to require,
 153+such as by intimate data communication or control flow between those
 154+subprograms and other parts of the work.
 155+
 156+ The Corresponding Source need not include anything that users
 157+can regenerate automatically from other parts of the Corresponding
 158+Source.
 159+
 160+ The Corresponding Source for a work in source code form is that
 161+same work.
 162+
 163+ 2. Basic Permissions.
 164+
 165+ All rights granted under this License are granted for the term of
 166+copyright on the Program, and are irrevocable provided the stated
 167+conditions are met. This License explicitly affirms your unlimited
 168+permission to run the unmodified Program. The output from running a
 169+covered work is covered by this License only if the output, given its
 170+content, constitutes a covered work. This License acknowledges your
 171+rights of fair use or other equivalent, as provided by copyright law.
 172+
 173+ You may make, run and propagate covered works that you do not
 174+convey, without conditions so long as your license otherwise remains
 175+in force. You may convey covered works to others for the sole purpose
 176+of having them make modifications exclusively for you, or provide you
 177+with facilities for running those works, provided that you comply with
 178+the terms of this License in conveying all material for which you do
 179+not control copyright. Those thus making or running the covered works
 180+for you must do so exclusively on your behalf, under your direction
 181+and control, on terms that prohibit them from making any copies of
 182+your copyrighted material outside their relationship with you.
 183+
 184+ Conveying under any other circumstances is permitted solely under
 185+the conditions stated below. Sublicensing is not allowed; section 10
 186+makes it unnecessary.
 187+
 188+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
 189+
 190+ No covered work shall be deemed part of an effective technological
 191+measure under any applicable law fulfilling obligations under article
 192+11 of the WIPO copyright treaty adopted on 20 December 1996, or
 193+similar laws prohibiting or restricting circumvention of such
 194+measures.
 195+
 196+ When you convey a covered work, you waive any legal power to forbid
 197+circumvention of technological measures to the extent such circumvention
 198+is effected by exercising rights under this License with respect to
 199+the covered work, and you disclaim any intention to limit operation or
 200+modification of the work as a means of enforcing, against the work's
 201+users, your or third parties' legal rights to forbid circumvention of
 202+technological measures.
 203+
 204+ 4. Conveying Verbatim Copies.
 205+
 206+ You may convey verbatim copies of the Program's source code as you
 207+receive it, in any medium, provided that you conspicuously and
 208+appropriately publish on each copy an appropriate copyright notice;
 209+keep intact all notices stating that this License and any
 210+non-permissive terms added in accord with section 7 apply to the code;
 211+keep intact all notices of the absence of any warranty; and give all
 212+recipients a copy of this License along with the Program.
 213+
 214+ You may charge any price or no price for each copy that you convey,
 215+and you may offer support or warranty protection for a fee.
 216+
 217+ 5. Conveying Modified Source Versions.
 218+
 219+ You may convey a work based on the Program, or the modifications to
 220+produce it from the Program, in the form of source code under the
 221+terms of section 4, provided that you also meet all of these conditions:
 222+
 223+ a) The work must carry prominent notices stating that you modified
 224+ it, and giving a relevant date.
 225+
 226+ b) The work must carry prominent notices stating that it is
 227+ released under this License and any conditions added under section
 228+ 7. This requirement modifies the requirement in section 4 to
 229+ "keep intact all notices".
 230+
 231+ c) You must license the entire work, as a whole, under this
 232+ License to anyone who comes into possession of a copy. This
 233+ License will therefore apply, along with any applicable section 7
 234+ additional terms, to the whole of the work, and all its parts,
 235+ regardless of how they are packaged. This License gives no
 236+ permission to license the work in any other way, but it does not
 237+ invalidate such permission if you have separately received it.
 238+
 239+ d) If the work has interactive user interfaces, each must display
 240+ Appropriate Legal Notices; however, if the Program has interactive
 241+ interfaces that do not display Appropriate Legal Notices, your
 242+ work need not make them do so.
 243+
 244+ A compilation of a covered work with other separate and independent
 245+works, which are not by their nature extensions of the covered work,
 246+and which are not combined with it such as to form a larger program,
 247+in or on a volume of a storage or distribution medium, is called an
 248+"aggregate" if the compilation and its resulting copyright are not
 249+used to limit the access or legal rights of the compilation's users
 250+beyond what the individual works permit. Inclusion of a covered work
 251+in an aggregate does not cause this License to apply to the other
 252+parts of the aggregate.
 253+
 254+ 6. Conveying Non-Source Forms.
 255+
 256+ You may convey a covered work in object code form under the terms
 257+of sections 4 and 5, provided that you also convey the
 258+machine-readable Corresponding Source under the terms of this License,
 259+in one of these ways:
 260+
 261+ a) Convey the object code in, or embodied in, a physical product
 262+ (including a physical distribution medium), accompanied by the
 263+ Corresponding Source fixed on a durable physical medium
 264+ customarily used for software interchange.
 265+
 266+ b) Convey the object code in, or embodied in, a physical product
 267+ (including a physical distribution medium), accompanied by a
 268+ written offer, valid for at least three years and valid for as
 269+ long as you offer spare parts or customer support for that product
 270+ model, to give anyone who possesses the object code either (1) a
 271+ copy of the Corresponding Source for all the software in the
 272+ product that is covered by this License, on a durable physical
 273+ medium customarily used for software interchange, for a price no
 274+ more than your reasonable cost of physically performing this
 275+ conveying of source, or (2) access to copy the
 276+ Corresponding Source from a network server at no charge.
 277+
 278+ c) Convey individual copies of the object code with a copy of the
 279+ written offer to provide the Corresponding Source. This
 280+ alternative is allowed only occasionally and noncommercially, and
 281+ only if you received the object code with such an offer, in accord
 282+ with subsection 6b.
 283+
 284+ d) Convey the object code by offering access from a designated
 285+ place (gratis or for a charge), and offer equivalent access to the
 286+ Corresponding Source in the same way through the same place at no
 287+ further charge. You need not require recipients to copy the
 288+ Corresponding Source along with the object code. If the place to
 289+ copy the object code is a network server, the Corresponding Source
 290+ may be on a different server (operated by you or a third party)
 291+ that supports equivalent copying facilities, provided you maintain
 292+ clear directions next to the object code saying where to find the
 293+ Corresponding Source. Regardless of what server hosts the
 294+ Corresponding Source, you remain obligated to ensure that it is
 295+ available for as long as needed to satisfy these requirements.
 296+
 297+ e) Convey the object code using peer-to-peer transmission, provided
 298+ you inform other peers where the object code and Corresponding
 299+ Source of the work are being offered to the general public at no
 300+ charge under subsection 6d.
 301+
 302+ A separable portion of the object code, whose source code is excluded
 303+from the Corresponding Source as a System Library, need not be
 304+included in conveying the object code work.
 305+
 306+ A "User Product" is either (1) a "consumer product", which means any
 307+tangible personal property which is normally used for personal, family,
 308+or household purposes, or (2) anything designed or sold for incorporation
 309+into a dwelling. In determining whether a product is a consumer product,
 310+doubtful cases shall be resolved in favor of coverage. For a particular
 311+product received by a particular user, "normally used" refers to a
 312+typical or common use of that class of product, regardless of the status
 313+of the particular user or of the way in which the particular user
 314+actually uses, or expects or is expected to use, the product. A product
 315+is a consumer product regardless of whether the product has substantial
 316+commercial, industrial or non-consumer uses, unless such uses represent
 317+the only significant mode of use of the product.
 318+
 319+ "Installation Information" for a User Product means any methods,
 320+procedures, authorization keys, or other information required to install
 321+and execute modified versions of a covered work in that User Product from
 322+a modified version of its Corresponding Source. The information must
 323+suffice to ensure that the continued functioning of the modified object
 324+code is in no case prevented or interfered with solely because
 325+modification has been made.
 326+
 327+ If you convey an object code work under this section in, or with, or
 328+specifically for use in, a User Product, and the conveying occurs as
 329+part of a transaction in which the right of possession and use of the
 330+User Product is transferred to the recipient in perpetuity or for a
 331+fixed term (regardless of how the transaction is characterized), the
 332+Corresponding Source conveyed under this section must be accompanied
 333+by the Installation Information. But this requirement does not apply
 334+if neither you nor any third party retains the ability to install
 335+modified object code on the User Product (for example, the work has
 336+been installed in ROM).
 337+
 338+ The requirement to provide Installation Information does not include a
 339+requirement to continue to provide support service, warranty, or updates
 340+for a work that has been modified or installed by the recipient, or for
 341+the User Product in which it has been modified or installed. Access to a
 342+network may be denied when the modification itself materially and
 343+adversely affects the operation of the network or violates the rules and
 344+protocols for communication across the network.
 345+
 346+ Corresponding Source conveyed, and Installation Information provided,
 347+in accord with this section must be in a format that is publicly
 348+documented (and with an implementation available to the public in
 349+source code form), and must require no special password or key for
 350+unpacking, reading or copying.
 351+
 352+ 7. Additional Terms.
 353+
 354+ "Additional permissions" are terms that supplement the terms of this
 355+License by making exceptions from one or more of its conditions.
 356+Additional permissions that are applicable to the entire Program shall
 357+be treated as though they were included in this License, to the extent
 358+that they are valid under applicable law. If additional permissions
 359+apply only to part of the Program, that part may be used separately
 360+under those permissions, but the entire Program remains governed by
 361+this License without regard to the additional permissions.
 362+
 363+ When you convey a copy of a covered work, you may at your option
 364+remove any additional permissions from that copy, or from any part of
 365+it. (Additional permissions may be written to require their own
 366+removal in certain cases when you modify the work.) You may place
 367+additional permissions on material, added by you to a covered work,
 368+for which you have or can give appropriate copyright permission.
 369+
 370+ Notwithstanding any other provision of this License, for material you
 371+add to a covered work, you may (if authorized by the copyright holders of
 372+that material) supplement the terms of this License with terms:
 373+
 374+ a) Disclaiming warranty or limiting liability differently from the
 375+ terms of sections 15 and 16 of this License; or
 376+
 377+ b) Requiring preservation of specified reasonable legal notices or
 378+ author attributions in that material or in the Appropriate Legal
 379+ Notices displayed by works containing it; or
 380+
 381+ c) Prohibiting misrepresentation of the origin of that material, or
 382+ requiring that modified versions of such material be marked in
 383+ reasonable ways as different from the original version; or
 384+
 385+ d) Limiting the use for publicity purposes of names of licensors or
 386+ authors of the material; or
 387+
 388+ e) Declining to grant rights under trademark law for use of some
 389+ trade names, trademarks, or service marks; or
 390+
 391+ f) Requiring indemnification of licensors and authors of that
 392+ material by anyone who conveys the material (or modified versions of
 393+ it) with contractual assumptions of liability to the recipient, for
 394+ any liability that these contractual assumptions directly impose on
 395+ those licensors and authors.
 396+
 397+ All other non-permissive additional terms are considered "further
 398+restrictions" within the meaning of section 10. If the Program as you
 399+received it, or any part of it, contains a notice stating that it is
 400+governed by this License along with a term that is a further
 401+restriction, you may remove that term. If a license document contains
 402+a further restriction but permits relicensing or conveying under this
 403+License, you may add to a covered work material governed by the terms
 404+of that license document, provided that the further restriction does
 405+not survive such relicensing or conveying.
 406+
 407+ If you add terms to a covered work in accord with this section, you
 408+must place, in the relevant source files, a statement of the
 409+additional terms that apply to those files, or a notice indicating
 410+where to find the applicable terms.
 411+
 412+ Additional terms, permissive or non-permissive, may be stated in the
 413+form of a separately written license, or stated as exceptions;
 414+the above requirements apply either way.
 415+
 416+ 8. Termination.
 417+
 418+ You may not propagate or modify a covered work except as expressly
 419+provided under this License. Any attempt otherwise to propagate or
 420+modify it is void, and will automatically terminate your rights under
 421+this License (including any patent licenses granted under the third
 422+paragraph of section 11).
 423+
 424+ However, if you cease all violation of this License, then your
 425+license from a particular copyright holder is reinstated (a)
 426+provisionally, unless and until the copyright holder explicitly and
 427+finally terminates your license, and (b) permanently, if the copyright
 428+holder fails to notify you of the violation by some reasonable means
 429+prior to 60 days after the cessation.
 430+
 431+ Moreover, your license from a particular copyright holder is
 432+reinstated permanently if the copyright holder notifies you of the
 433+violation by some reasonable means, this is the first time you have
 434+received notice of violation of this License (for any work) from that
 435+copyright holder, and you cure the violation prior to 30 days after
 436+your receipt of the notice.
 437+
 438+ Termination of your rights under this section does not terminate the
 439+licenses of parties who have received copies or rights from you under
 440+this License. If your rights have been terminated and not permanently
 441+reinstated, you do not qualify to receive new licenses for the same
 442+material under section 10.
 443+
 444+ 9. Acceptance Not Required for Having Copies.
 445+
 446+ You are not required to accept this License in order to receive or
 447+run a copy of the Program. Ancillary propagation of a covered work
 448+occurring solely as a consequence of using peer-to-peer transmission
 449+to receive a copy likewise does not require acceptance. However,
 450+nothing other than this License grants you permission to propagate or
 451+modify any covered work. These actions infringe copyright if you do
 452+not accept this License. Therefore, by modifying or propagating a
 453+covered work, you indicate your acceptance of this License to do so.
 454+
 455+ 10. Automatic Licensing of Downstream Recipients.
 456+
 457+ Each time you convey a covered work, the recipient automatically
 458+receives a license from the original licensors, to run, modify and
 459+propagate that work, subject to this License. You are not responsible
 460+for enforcing compliance by third parties with this License.
 461+
 462+ An "entity transaction" is a transaction transferring control of an
 463+organization, or substantially all assets of one, or subdividing an
 464+organization, or merging organizations. If propagation of a covered
 465+work results from an entity transaction, each party to that
 466+transaction who receives a copy of the work also receives whatever
 467+licenses to the work the party's predecessor in interest had or could
 468+give under the previous paragraph, plus a right to possession of the
 469+Corresponding Source of the work from the predecessor in interest, if
 470+the predecessor has it or can get it with reasonable efforts.
 471+
 472+ You may not impose any further restrictions on the exercise of the
 473+rights granted or affirmed under this License. For example, you may
 474+not impose a license fee, royalty, or other charge for exercise of
 475+rights granted under this License, and you may not initiate litigation
 476+(including a cross-claim or counterclaim in a lawsuit) alleging that
 477+any patent claim is infringed by making, using, selling, offering for
 478+sale, or importing the Program or any portion of it.
 479+
 480+ 11. Patents.
 481+
 482+ A "contributor" is a copyright holder who authorizes use under this
 483+License of the Program or a work on which the Program is based. The
 484+work thus licensed is called the contributor's "contributor version".
 485+
 486+ A contributor's "essential patent claims" are all patent claims
 487+owned or controlled by the contributor, whether already acquired or
 488+hereafter acquired, that would be infringed by some manner, permitted
 489+by this License, of making, using, or selling its contributor version,
 490+but do not include claims that would be infringed only as a
 491+consequence of further modification of the contributor version. For
 492+purposes of this definition, "control" includes the right to grant
 493+patent sublicenses in a manner consistent with the requirements of
 494+this License.
 495+
 496+ Each contributor grants you a non-exclusive, worldwide, royalty-free
 497+patent license under the contributor's essential patent claims, to
 498+make, use, sell, offer for sale, import and otherwise run, modify and
 499+propagate the contents of its contributor version.
 500+
 501+ In the following three paragraphs, a "patent license" is any express
 502+agreement or commitment, however denominated, not to enforce a patent
 503+(such as an express permission to practice a patent or covenant not to
 504+sue for patent infringement). To "grant" such a patent license to a
 505+party means to make such an agreement or commitment not to enforce a
 506+patent against the party.
 507+
 508+ If you convey a covered work, knowingly relying on a patent license,
 509+and the Corresponding Source of the work is not available for anyone
 510+to copy, free of charge and under the terms of this License, through a
 511+publicly available network server or other readily accessible means,
 512+then you must either (1) cause the Corresponding Source to be so
 513+available, or (2) arrange to deprive yourself of the benefit of the
 514+patent license for this particular work, or (3) arrange, in a manner
 515+consistent with the requirements of this License, to extend the patent
 516+license to downstream recipients. "Knowingly relying" means you have
 517+actual knowledge that, but for the patent license, your conveying the
 518+covered work in a country, or your recipient's use of the covered work
 519+in a country, would infringe one or more identifiable patents in that
 520+country that you have reason to believe are valid.
 521+
 522+ If, pursuant to or in connection with a single transaction or
 523+arrangement, you convey, or propagate by procuring conveyance of, a
 524+covered work, and grant a patent license to some of the parties
 525+receiving the covered work authorizing them to use, propagate, modify
 526+or convey a specific copy of the covered work, then the patent license
 527+you grant is automatically extended to all recipients of the covered
 528+work and works based on it.
 529+
 530+ A patent license is "discriminatory" if it does not include within
 531+the scope of its coverage, prohibits the exercise of, or is
 532+conditioned on the non-exercise of one or more of the rights that are
 533+specifically granted under this License. You may not convey a covered
 534+work if you are a party to an arrangement with a third party that is
 535+in the business of distributing software, under which you make payment
 536+to the third party based on the extent of your activity of conveying
 537+the work, and under which the third party grants, to any of the
 538+parties who would receive the covered work from you, a discriminatory
 539+patent license (a) in connection with copies of the covered work
 540+conveyed by you (or copies made from those copies), or (b) primarily
 541+for and in connection with specific products or compilations that
 542+contain the covered work, unless you entered into that arrangement,
 543+or that patent license was granted, prior to 28 March 2007.
 544+
 545+ Nothing in this License shall be construed as excluding or limiting
 546+any implied license or other defenses to infringement that may
 547+otherwise be available to you under applicable patent law.
 548+
 549+ 12. No Surrender of Others' Freedom.
 550+
 551+ If conditions are imposed on you (whether by court order, agreement or
 552+otherwise) that contradict the conditions of this License, they do not
 553+excuse you from the conditions of this License. If you cannot convey a
 554+covered work so as to satisfy simultaneously your obligations under this
 555+License and any other pertinent obligations, then as a consequence you may
 556+not convey it at all. For example, if you agree to terms that obligate you
 557+to collect a royalty for further conveying from those to whom you convey
 558+the Program, the only way you could satisfy both those terms and this
 559+License would be to refrain entirely from conveying the Program.
 560+
 561+ 13. Use with the GNU Affero General Public License.
 562+
 563+ Notwithstanding any other provision of this License, you have
 564+permission to link or combine any covered work with a work licensed
 565+under version 3 of the GNU Affero General Public License into a single
 566+combined work, and to convey the resulting work. The terms of this
 567+License will continue to apply to the part which is the covered work,
 568+but the special requirements of the GNU Affero General Public License,
 569+section 13, concerning interaction through a network will apply to the
 570+combination as such.
 571+
 572+ 14. Revised Versions of this License.
 573+
 574+ The Free Software Foundation may publish revised and/or new versions of
 575+the GNU General Public License from time to time. Such new versions will
 576+be similar in spirit to the present version, but may differ in detail to
 577+address new problems or concerns.
 578+
 579+ Each version is given a distinguishing version number. If the
 580+Program specifies that a certain numbered version of the GNU General
 581+Public License "or any later version" applies to it, you have the
 582+option of following the terms and conditions either of that numbered
 583+version or of any later version published by the Free Software
 584+Foundation. If the Program does not specify a version number of the
 585+GNU General Public License, you may choose any version ever published
 586+by the Free Software Foundation.
 587+
 588+ If the Program specifies that a proxy can decide which future
 589+versions of the GNU General Public License can be used, that proxy's
 590+public statement of acceptance of a version permanently authorizes you
 591+to choose that version for the Program.
 592+
 593+ Later license versions may give you additional or different
 594+permissions. However, no additional obligations are imposed on any
 595+author or copyright holder as a result of your choosing to follow a
 596+later version.
 597+
 598+ 15. Disclaimer of Warranty.
 599+
 600+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
 601+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
 602+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
 603+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
 604+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 605+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
 606+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
 607+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
 608+
 609+ 16. Limitation of Liability.
 610+
 611+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
 612+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
 613+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
 614+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
 615+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
 616+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
 617+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
 618+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
 619+SUCH DAMAGES.
 620+
 621+ 17. Interpretation of Sections 15 and 16.
 622+
 623+ If the disclaimer of warranty and limitation of liability provided
 624+above cannot be given local legal effect according to their terms,
 625+reviewing courts shall apply local law that most closely approximates
 626+an absolute waiver of all civil liability in connection with the
 627+Program, unless a warranty or assumption of liability accompanies a
 628+copy of the Program in return for a fee.
 629+
 630+ END OF TERMS AND CONDITIONS
 631+
 632+ How to Apply These Terms to Your New Programs
 633+
 634+ If you develop a new program, and you want it to be of the greatest
 635+possible use to the public, the best way to achieve this is to make it
 636+free software which everyone can redistribute and change under these terms.
 637+
 638+ To do so, attach the following notices to the program. It is safest
 639+to attach them to the start of each source file to most effectively
 640+state the exclusion of warranty; and each file should have at least
 641+the "copyright" line and a pointer to where the full notice is found.
 642+
 643+ <one line to give the program's name and a brief idea of what it does.>
 644+ Copyright (C) <year> <name of author>
 645+
 646+ This program is free software: you can redistribute it and/or modify
 647+ it under the terms of the GNU General Public License as published by
 648+ the Free Software Foundation, either version 3 of the License, or
 649+ (at your option) any later version.
 650+
 651+ This program is distributed in the hope that it will be useful,
 652+ but WITHOUT ANY WARRANTY; without even the implied warranty of
 653+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 654+ GNU General Public License for more details.
 655+
 656+ You should have received a copy of the GNU General Public License
 657+ along with this program. If not, see <http://www.gnu.org/licenses/>.
 658+
 659+Also add information on how to contact you by electronic and paper mail.
 660+
 661+ If the program does terminal interaction, make it output a short
 662+notice like this when it starts in an interactive mode:
 663+
 664+ <program> Copyright (C) <year> <name of author>
 665+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
 666+ This is free software, and you are welcome to redistribute it
 667+ under certain conditions; type `show c' for details.
 668+
 669+The hypothetical commands `show w' and `show c' should show the appropriate
 670+parts of the General Public License. Of course, your program's commands
 671+might be different; for a GUI interface, you would use an "about box".
 672+
 673+ You should also get your employer (if you work as a programmer) or school,
 674+if any, to sign a "copyright disclaimer" for the program, if necessary.
 675+For more information on this, and how to apply and follow the GNU GPL, see
 676+<http://www.gnu.org/licenses/>.
 677+
 678+ The GNU General Public License does not permit incorporating your program
 679+into proprietary programs. If your program is a subroutine library, you
 680+may consider it more useful to permit linking proprietary applications with
 681+the library. If this is what you want to do, use the GNU Lesser General
 682+Public License instead of this License. But first, please read
 683+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
\ No newline at end of file
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/COPYING
___________________________________________________________________
Added: svn:eol-style
1684 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/api/ApiDeleteContest.php
@@ -0,0 +1,98 @@
 2+<?php
 3+
 4+/**
 5+ * API module to delete contests.
 6+ *
 7+ * @since 0.1
 8+ *
 9+ * @file ApiDeleteContest.php
 10+ * @ingroup Contest
 11+ * @ingroup API
 12+ *
 13+ * @licence GNU GPL v3+
 14+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
 15+ */
 16+class ApiDeleteContest extends ApiBase {
 17+
 18+ public function __construct( $main, $action ) {
 19+ parent::__construct( $main, $action );
 20+ }
 21+
 22+ public function execute() {
 23+ global $wgUser;
 24+
 25+ if ( !$wgUser->isAllowed( 'contestadmin' ) || $wgUser->isBlocked() ) {
 26+ $this->dieUsageMsg( array( 'badaccess-groups' ) );
 27+ }
 28+
 29+ $params = $this->extractRequestParams();
 30+
 31+ $everythingOk = true;
 32+
 33+ foreach ( $params['ids'] as $id ) {
 34+ $contest = new Contest( array( 'id' => $id ) );
 35+ $everythingOk = $contest->removeAllFromDB() && $everythingOk;
 36+ }
 37+
 38+ $this->getResult()->addValue(
 39+ null,
 40+ 'success',
 41+ $everythingOk
 42+ );
 43+ }
 44+
 45+ public function needsToken() {
 46+ return true;
 47+ }
 48+
 49+ public function getTokenSalt() {
 50+ $params = $this->extractRequestParams();
 51+ return 'deletecontest' . implode( '|', $params['ids'] );
 52+ }
 53+
 54+ public function mustBePosted() {
 55+ return true;
 56+ }
 57+
 58+ public function getAllowedParams() {
 59+ return array(
 60+ 'ids' => array(
 61+ ApiBase::PARAM_TYPE => 'integer',
 62+ ApiBase::PARAM_REQUIRED => true,
 63+ ApiBase::PARAM_ISMULTI => true,
 64+ ),
 65+ 'token' => null,
 66+ );
 67+ }
 68+
 69+ public function getParamDescription() {
 70+ return array(
 71+ 'ids' => 'The IDs of the contests to delete',
 72+ 'token' => 'Edit token, salted with the contest id',
 73+ );
 74+ }
 75+
 76+ public function getDescription() {
 77+ return array(
 78+ 'API module for deleting contests.'
 79+ );
 80+ }
 81+
 82+ public function getPossibleErrors() {
 83+ return array_merge( parent::getPossibleErrors(), array(
 84+ array( 'missingparam', 'ids' ),
 85+ ) );
 86+ }
 87+
 88+ protected function getExamples() {
 89+ return array(
 90+ 'api.php?action=deletecontest&ids=42',
 91+ 'api.php?action=deletecontest&ids=4|2',
 92+ );
 93+ }
 94+
 95+ public function getVersion() {
 96+ return __CLASS__ . ': $Id$';
 97+ }
 98+
 99+}
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/api/ApiDeleteContest.php
___________________________________________________________________
Added: svn:keywords
1100 + Id
Added: svn:eol-style
2101 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/api/ApiContestQuery.php
@@ -0,0 +1,231 @@
 2+<?php
 3+
 4+/**
 5+ * Base class for API query modules that return results using a
 6+ * ContestDBObject deriving class.
 7+ *
 8+ * @since 0.1
 9+ *
 10+ * @file ApiContestQuery.php
 11+ * @ingroup Contest
 12+ * @ingroup API
 13+ *
 14+ * @licence GNU GPL v3+
 15+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
 16+ */
 17+abstract class ApiContestQuery extends ApiQueryBase {
 18+
 19+ /**
 20+ * Returns the class specific info.
 21+ * * class: name of the ContestDBObject deriving class (ie Contest)
 22+ * * item: item name (ie contest)
 23+ * * set: item set name (ie contests)
 24+ *
 25+ * @since 0.1
 26+ *
 27+ * @return array of string
 28+ */
 29+ protected abstract function getClassInfo();
 30+
 31+ /**
 32+ * Returns an instance of the ContestDBObject deriving class.
 33+ * Once PHP 5.3 becomes an acceptable requirement, we
 34+ * can get rid of this silly hack and simply return the class
 35+ * name (since all methods we need ought to be static in PHP >= 5.3).
 36+ *
 37+ * @since 0.1
 38+ *
 39+ * @return ContestDBObject
 40+ */
 41+ protected function getClass() {
 42+ $className = $this->getClassInfo();
 43+ $className = $className['class'];
 44+ return $className::s();
 45+ }
 46+
 47+ /**
 48+ * Get the parameters, find out what the conditions for the query are,
 49+ * run it, and add the results.
 50+ *
 51+ * @since 0.1
 52+ */
 53+ public function execute() {
 54+ $params = $this->getParams();
 55+
 56+ if ( count( $params['props'] ) > 0 ) {
 57+ $results = $this->getResults( $params, $this->getConditions( $params ) );
 58+ $this->addResults( $params, $results );
 59+ }
 60+ }
 61+
 62+ /**
 63+ * Get the request parameters, handle the * value for the props param
 64+ * and remove all params set to null (ie those that are not actually provided).
 65+ *
 66+ * @since 0.1
 67+ *
 68+ * @return array
 69+ */
 70+ protected function getParams() {
 71+ // Get the requests parameters.
 72+ $params = $this->extractRequestParams();
 73+
 74+ $starPropPosition = array_search( '*', $params['props'] );
 75+
 76+ if ( $starPropPosition !== false ) {
 77+ unset( $params['props'][$starPropPosition] );
 78+ $params['props'] = array_merge( $params['props'], $this->getClass()->getFieldNames() );
 79+ }
 80+
 81+ return array_filter( $params, create_function( '$p', 'return isset( $p );' ) );
 82+ }
 83+
 84+ /**
 85+ * Get the conditions for the query. These will be provided as
 86+ * regular parameters, together with limit, props, continue,
 87+ * and possibly others which we need to get rid off.
 88+ *
 89+ * @since 0.1
 90+ *
 91+ * @param array $params
 92+ *
 93+ * @return array
 94+ */
 95+ protected function getConditions( array $params ) {
 96+ $conditions = array();
 97+
 98+ foreach ( $params as $name => $value ) {
 99+ if ( $this->getClass()->canHasField( $name ) ) {
 100+ $conditions[$name] = $value;
 101+ }
 102+ }
 103+
 104+ return $conditions;
 105+ }
 106+
 107+ /**
 108+ * Get the actual results.
 109+ *
 110+ * @since 0.1
 111+ *
 112+ * @param array $params
 113+ * @param array $conditions
 114+ *
 115+ * @return array of ContestDBClass
 116+ */
 117+ protected function getResults( array $params, array $conditions ) {
 118+ return $this->getClass()->select(
 119+ $params['props'],
 120+ $conditions,
 121+ array(
 122+ 'LIMIT' => $params['limit'] + 1,
 123+ 'ORDER BY' => $this->getClass()->getPrefixedField( 'id' ) . ' ASC'
 124+ )
 125+ );
 126+ }
 127+
 128+ /**
 129+ * Serialize the results and add them to the result object.
 130+ *
 131+ * @since 0.1
 132+ *
 133+ * @param array $params
 134+ * @param array $results
 135+ */
 136+ protected function addResults( array $params, array /* of ContestDBObject */ $results ) {
 137+ $serializedResults = array();
 138+ $count = 0;
 139+
 140+ foreach ( $results as $result ) {
 141+ if ( ++$count > $params['limit'] ) {
 142+ // We've reached the one extra which shows that
 143+ // there are additional pages to be had. Stop here...
 144+ $this->setContinueEnumParameter( 'continue', $result->getId() );
 145+ break;
 146+ }
 147+
 148+ $serializedResults[] = $result->toArray( $params['props'] );
 149+ }
 150+
 151+ $this->setIndexedTagNames( $serializedResults );
 152+ $this->addSerializedResults( $serializedResults );
 153+ }
 154+
 155+ /**
 156+ * Set the tag names for formats such as XML.
 157+ *
 158+ * @since 0.1
 159+ *
 160+ * @param array $serializedResults
 161+ */
 162+ protected function setIndexedTagNames( array &$serializedResults ) {
 163+ $classInfo = $this->getClassInfo();
 164+ $this->getResult()->setIndexedTagName( $serializedResults, $classInfo['item'] );
 165+ }
 166+
 167+ /**
 168+ * Add the serialized results to the result object.
 169+ *
 170+ * @since 0.1
 171+ *
 172+ * @param array $serializedResults
 173+ */
 174+ protected function addSerializedResults( array $serializedResults ) {
 175+ $classInfo = $this->getClassInfo();
 176+
 177+ $this->getResult()->addValue(
 178+ null,
 179+ $classInfo['set'],
 180+ $serializedResults
 181+ );
 182+ }
 183+
 184+ /**
 185+ * (non-PHPdoc)
 186+ * @see includes/api/ApiBase#getAllowedParams()
 187+ * @return array
 188+ */
 189+ public function getAllowedParams() {
 190+ $params = array (
 191+ 'props' => array(
 192+ ApiBase::PARAM_TYPE => array_merge( $this->getClass()->getFieldNames(), array( '*' ) ),
 193+ ApiBase::PARAM_ISMULTI => true,
 194+ ApiBase::PARAM_DFLT => '*'
 195+ ),
 196+ 'limit' => array(
 197+ ApiBase::PARAM_DFLT => 20,
 198+ ApiBase::PARAM_TYPE => 'limit',
 199+ ApiBase::PARAM_MIN => 1,
 200+ ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1,
 201+ ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2
 202+ ),
 203+ 'continue' => null,
 204+ );
 205+
 206+ return array_merge( $this->getClass()->getAPIParams(), $params );
 207+ }
 208+
 209+ /**
 210+ * (non-PHPdoc)
 211+ * @see includes/api/ApiBase#getParamDescription()
 212+ */
 213+ public function getParamDescription() {
 214+ $descs = array (
 215+ 'props' => 'Fields to query',
 216+ 'continue' => 'Offset number from where to continue the query',
 217+ 'limit' => 'Max amount of rows to return',
 218+ );
 219+
 220+ return array_merge( $this->getClass()->getFieldDescriptions(), $descs );
 221+ }
 222+
 223+ /**
 224+ * (non-PHPdoc)
 225+ * @see includes/api/ApiBase#getPossibleErrors()
 226+ */
 227+ public function getPossibleErrors() {
 228+ return array_merge( parent::getPossibleErrors(), array(
 229+ ) );
 230+ }
 231+
 232+}
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/api/ApiContestQuery.php
___________________________________________________________________
Added: svn:eol-style
1233 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/api/ApiMailContestants.php
@@ -0,0 +1,197 @@
 2+<?php
 3+
 4+/**
 5+ * API module to mail contestants.
 6+ *
 7+ * @since 0.1
 8+ *
 9+ * @file ApiMailContestants.php
 10+ * @ingroup Contest
 11+ * @ingroup API
 12+ *
 13+ * @licence GNU GPL v3+
 14+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
 15+ */
 16+class ApiMailContestants extends ApiBase {
 17+
 18+ public function __construct( $main, $action ) {
 19+ parent::__construct( $main, $action );
 20+ }
 21+
 22+ // TODO
 23+ public function execute() {
 24+ global $wgUser;
 25+
 26+ if ( !$wgUser->isAllowed( 'contestadmin' ) || $wgUser->isBlocked() ) {
 27+ $this->dieUsageMsg( array( 'badaccess-groups' ) );
 28+ }
 29+
 30+ $params = $this->extractRequestParams();
 31+
 32+ $everythingOk = true;
 33+
 34+ $contestIds = is_null( $params['contestids'] ) ? array() : $params['contestids'];
 35+ $challengeIds = is_null( $params['challengeids'] ) ? array() : $params['challengeids'];
 36+
 37+ if ( !is_null( $params['challengetitles'] ) ) {
 38+ $challenges = ContestChallenge::s()->select( 'id', array( 'title' => $params['challengetitles'] ) );
 39+
 40+ if ( $challenges === false ) {
 41+ // TODO: error
 42+ }
 43+
 44+ foreach ( $challenges as /* ContestChallenge */ $challenge ) {
 45+ $challengeIds[] = $challenge->getId();
 46+ }
 47+ }
 48+
 49+ if ( !is_null( $params['contestnames'] ) ) {
 50+ $contests = Contest::s()->select( 'id', array( 'name' => $params['contestnames'] ) );
 51+
 52+ if ( $contests === false ) {
 53+ // TODO: error
 54+ }
 55+
 56+ foreach ( $contests as /* Contest */ $contest ) {
 57+ $contestIds[] = $contest->getId();
 58+ }
 59+ }
 60+
 61+ $conditions = array();
 62+
 63+ if ( count( $contestIds ) > 0 ) {
 64+ $conditions['contest_id'] = $contestIds;
 65+ }
 66+
 67+ if ( count( $challengeIds ) > 0 ) {
 68+ $conditions['challenge_id'] = $challengeIds;
 69+ }
 70+
 71+ if ( !is_null( $params['ids'] ) && count( $params['ids'] ) > 0 ) {
 72+ $conditions['id'] = $params['ids'];
 73+ }
 74+
 75+ $contestants = ContestContestant::s()->select( 'email', $conditions );
 76+
 77+ if ( $contestants !== false && count( $contestants ) > 0 ) {
 78+ $setSize = ContestSettings::get( 'reminderJobSize' );
 79+ $limit = count( $contestants ) - $setSize;
 80+
 81+ for ( $i = 0; $i <= $limit; $i += $setSize ) {
 82+ $this->createReminderJob( array_splice( $contestants, $i, $setSize ) );
 83+ }
 84+ }
 85+ else {
 86+ $everythingOk = false;
 87+ }
 88+
 89+ $this->getResult()->addValue(
 90+ null,
 91+ 'success',
 92+ $everythingOk
 93+ );
 94+
 95+ if ( $everythingOk ) {
 96+ $this->getResult()->addValue(
 97+ null,
 98+ 'contestantcount',
 99+ count( $contestants )
 100+ );
 101+ }
 102+ }
 103+
 104+ protected function createReminderJob( array /* of ContestContestant */ $contestants ) {
 105+ $job = new ContestReminderJob(
 106+ Title::newMainPage(), // WTF does this require a title for??
 107+ array(
 108+ 'contest' => $contestants[0]->getContest(),
 109+ 'contestants' => $contestants
 110+ )
 111+ );
 112+ $job->insert();
 113+ }
 114+
 115+ public function needsToken() {
 116+ return true;
 117+ }
 118+
 119+ public function mustBePosted() {
 120+ return true;
 121+ }
 122+
 123+ public function getAllowedParams() {
 124+ return array(
 125+// 'page' => array(
 126+// ApiBase::PARAM_TYPE => 'string',
 127+// ApiBase::PARAM_REQUIRED => true,
 128+// ApiBase::PARAM_ISMULTI => false,
 129+// ),
 130+ 'ids' => array(
 131+ ApiBase::PARAM_TYPE => 'integer',
 132+ ApiBase::PARAM_REQUIRED => false,
 133+ ApiBase::PARAM_ISMULTI => true,
 134+ ),
 135+ 'contestids' => array(
 136+ ApiBase::PARAM_TYPE => 'integer',
 137+ ApiBase::PARAM_REQUIRED => false,
 138+ ApiBase::PARAM_ISMULTI => true,
 139+ ),
 140+ 'contestnames' => array(
 141+ ApiBase::PARAM_TYPE => 'string',
 142+ ApiBase::PARAM_REQUIRED => false,
 143+ ApiBase::PARAM_ISMULTI => true,
 144+ ),
 145+ 'challengeids' => array(
 146+ ApiBase::PARAM_TYPE => 'integer',
 147+ ApiBase::PARAM_REQUIRED => false,
 148+ ApiBase::PARAM_ISMULTI => true,
 149+ ),
 150+ 'challengetitles' => array(
 151+ ApiBase::PARAM_TYPE => 'string',
 152+ ApiBase::PARAM_REQUIRED => false,
 153+ ApiBase::PARAM_ISMULTI => true,
 154+ ),
 155+ 'token' => null,
 156+ );
 157+ }
 158+
 159+ public function getParamDescription() {
 160+ return array(
 161+// 'page' => 'Name of the page from which to pull content for the email body',
 162+ 'ids' => 'The IDs of the contestants to mail',
 163+ 'contestids' => 'The IDs of the contests where of the contestants should be mailed',
 164+ 'contestnames' => 'The names of the contests where of the contestants should be mailed',
 165+ 'challengeids' => 'The IDs of the challenges where of the contestants should be mailed',
 166+ 'challengetitles' => 'The titles of the challenges where of the contestants should be mailed',
 167+ 'token' => 'Edit token',
 168+ );
 169+ }
 170+
 171+ public function getDescription() {
 172+ return array(
 173+ 'API module for mailing contestants. Selection criteria will be joined with AND,
 174+ except for the challange ids/titles and contest ids/names pairs, which will be joined wit OR.'
 175+ );
 176+ }
 177+
 178+ public function getPossibleErrors() {
 179+ return array_merge( parent::getPossibleErrors(), array(
 180+ array( 'missingparam', 'ids' ),
 181+ ) );
 182+ }
 183+
 184+ protected function getExamples() {
 185+ return array(
 186+ 'api.php?action=mailcontestants&ids=42',
 187+ 'api.php?action=mailcontestants&ids=4|2',
 188+ 'api.php?action=mailcontestants&contestids=42',
 189+ 'api.php?action=mailcontestants&contestnames=Weekend of Code',
 190+ 'api.php?action=mailcontestants&challengetitles=foo|bar|baz',
 191+ );
 192+ }
 193+
 194+ public function getVersion() {
 195+ return __CLASS__ . ': $Id$';
 196+ }
 197+
 198+}
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/api/ApiMailContestants.php
___________________________________________________________________
Added: svn:keywords
1199 + Id
Added: svn:eol-style
2200 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/api/ApiQueryContests.php
@@ -0,0 +1,75 @@
 2+<?php
 3+
 4+/**
 5+ * API module to get a list of contests.
 6+ *
 7+ * @since 0.1
 8+ *
 9+ * @file ApiQueryContests.php
 10+ * @ingroup Contest
 11+ * @ingroup API
 12+ *
 13+ * @licence GNU GPL v3+
 14+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
 15+ */
 16+class ApiQueryContests extends ApiContestQuery {
 17+
 18+ /**
 19+ * (non-PHPdoc)
 20+ * @see ApiContestQuery::getClassInfo()
 21+ * @return array
 22+ */
 23+ protected function getClassInfo() {
 24+ return array(
 25+ 'class' => 'Contest',
 26+ 'item' => 'contest',
 27+ 'set' => 'contests',
 28+ );
 29+ }
 30+
 31+ public function __construct( $main, $action ) {
 32+ parent::__construct( $main, $action, 'co' );
 33+ }
 34+
 35+ /**
 36+ * Handle the API request.
 37+ * Checks for access rights and then let's the parent method do the actual work.
 38+ */
 39+ public function execute() {
 40+ global $wgUser;
 41+
 42+ if ( !$wgUser->isAllowed( 'contestadmin' ) || $wgUser->isBlocked() ) {
 43+ $this->dieUsageMsg( array( 'badaccess-groups' ) );
 44+ }
 45+
 46+ parent::execute();
 47+ }
 48+
 49+ /**
 50+ * (non-PHPdoc)
 51+ * @see includes/api/ApiBase#getDescription()
 52+ */
 53+ public function getDescription() {
 54+ return 'API module for querying contests';
 55+ }
 56+
 57+ /**
 58+ * (non-PHPdoc)
 59+ * @see includes/api/ApiBase#getExamples()
 60+ */
 61+ protected function getExamples() {
 62+ return array (
 63+ 'api.php?action=query&list=contests&coprops=id|name',
 64+ 'api.php?action=query&list=contests&costatus=1',
 65+ );
 66+ }
 67+
 68+ /**
 69+ * (non-PHPdoc)
 70+ * @see includes/api/ApiBase#getVersion()
 71+ */
 72+ public function getVersion() {
 73+ return __CLASS__ . ': $Id$';
 74+ }
 75+
 76+}
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/api/ApiQueryContests.php
___________________________________________________________________
Added: svn:keywords
177 + Id
Added: svn:eol-style
278 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/api/ApiQueryChallenges.php
@@ -0,0 +1,75 @@
 2+<?php
 3+
 4+/**
 5+ * API module to get a list of contest challenges.
 6+ *
 7+ * @since 0.1
 8+ *
 9+ * @file ApiQueryChallenges.php
 10+ * @ingroup Contest
 11+ * @ingroup API
 12+ *
 13+ * @licence GNU GPL v3+
 14+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
 15+ */
 16+class ApiQueryChallenges extends ApiContestQuery {
 17+
 18+ /**
 19+ * (non-PHPdoc)
 20+ * @see ApiContestQuery::getClassInfo()
 21+ * @return array
 22+ */
 23+ protected function getClassInfo() {
 24+ return array(
 25+ 'class' => 'ContestChallenge',
 26+ 'item' => 'challenge',
 27+ 'set' => 'challenges',
 28+ );
 29+ }
 30+
 31+ public function __construct( $main, $action ) {
 32+ parent::__construct( $main, $action, 'ch' );
 33+ }
 34+
 35+ /**
 36+ * Handle the API request.
 37+ * Checks for access rights and then let's the parent method do the actual work.
 38+ */
 39+ public function execute() {
 40+ global $wgUser;
 41+
 42+ if ( !$wgUser->isAllowed( 'contestadmin' ) || $wgUser->isBlocked() ) {
 43+ $this->dieUsageMsg( array( 'badaccess-groups' ) );
 44+ }
 45+
 46+ parent::execute();
 47+ }
 48+
 49+ /**
 50+ * (non-PHPdoc)
 51+ * @see includes/api/ApiBase#getDescription()
 52+ */
 53+ public function getDescription() {
 54+ return 'API module for querying contest challenges';
 55+ }
 56+
 57+ /**
 58+ * (non-PHPdoc)
 59+ * @see includes/api/ApiBase#getExamples()
 60+ */
 61+ protected function getExamples() {
 62+ return array (
 63+ 'api.php?action=query&list=challenges&chprops=title|text',
 64+ 'api.php?action=query&list=challenges&chcontestid=42&chprops=id|contest_id|title',
 65+ );
 66+ }
 67+
 68+ /**
 69+ * (non-PHPdoc)
 70+ * @see includes/api/ApiBase#getVersion()
 71+ */
 72+ public function getVersion() {
 73+ return __CLASS__ . ': $Id$';
 74+ }
 75+
 76+}
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/api/ApiQueryChallenges.php
___________________________________________________________________
Added: svn:keywords
177 + Id
Added: svn:eol-style
278 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/api/ApiQueryContestComments.php
@@ -0,0 +1,75 @@
 2+<?php
 3+
 4+/**
 5+ * API module to get a list of commets.
 6+ *
 7+ * @since 0.1
 8+ *
 9+ * @file ApiQueryContestComments.php
 10+ * @ingroup Contest
 11+ * @ingroup API
 12+ *
 13+ * @licence GNU GPL v3+
 14+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
 15+ */
 16+class ApiQueryContestComments extends ApiContestQuery {
 17+
 18+ /**
 19+ * (non-PHPdoc)
 20+ * @see ApiContestQuery::getClassInfo()
 21+ * @return array
 22+ */
 23+ protected function getClassInfo() {
 24+ return array(
 25+ 'class' => 'ContestComment',
 26+ 'item' => 'comment',
 27+ 'set' => 'comments',
 28+ );
 29+ }
 30+
 31+ public function __construct( $main, $action ) {
 32+ parent::__construct( $main, $action, 'coco' );
 33+ }
 34+
 35+ /**
 36+ * Handle the API request.
 37+ * Checks for access rights and then let's the parent method do the actual work.
 38+ */
 39+ public function execute() {
 40+ global $wgUser;
 41+
 42+ if ( !$wgUser->isAllowed( 'contestjudge' ) || $wgUser->isBlocked() ) {
 43+ $this->dieUsageMsg( array( 'badaccess-groups' ) );
 44+ }
 45+
 46+ parent::execute();
 47+ }
 48+
 49+ /**
 50+ * (non-PHPdoc)
 51+ * @see includes/api/ApiBase#getDescription()
 52+ */
 53+ public function getDescription() {
 54+ return 'API module for querying contest comments';
 55+ }
 56+
 57+ /**
 58+ * (non-PHPdoc)
 59+ * @see includes/api/ApiBase#getExamples()
 60+ */
 61+ protected function getExamples() {
 62+ return array (
 63+ 'api.php?action=query&list=contestcomments&cocoprops=id|user_id|contestant_id|text',
 64+ 'api.php?action=query&list=contestcomments&cocoprops=id|text&cocouser_id=42',
 65+ );
 66+ }
 67+
 68+ /**
 69+ * (non-PHPdoc)
 70+ * @see includes/api/ApiBase#getVersion()
 71+ */
 72+ public function getVersion() {
 73+ return __CLASS__ . ': $Id$';
 74+ }
 75+
 76+}
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/api/ApiQueryContestComments.php
___________________________________________________________________
Added: svn:keywords
177 + Id
Added: svn:eol-style
278 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/api/ApiQueryContestants.php
@@ -0,0 +1,75 @@
 2+<?php
 3+
 4+/**
 5+ * API module to get a list of contestants.
 6+ *
 7+ * @since 0.1
 8+ *
 9+ * @file ApiQueryContestants.php
 10+ * @ingroup Contest
 11+ * @ingroup API
 12+ *
 13+ * @licence GNU GPL v3+
 14+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
 15+ */
 16+class ApiQueryContestants extends ApiContestQuery {
 17+
 18+ /**
 19+ * (non-PHPdoc)
 20+ * @see ApiContestQuery::getClassInfo()
 21+ * @return array
 22+ */
 23+ protected function getClassInfo() {
 24+ return array(
 25+ 'class' => 'ContestContestant',
 26+ 'item' => 'contestant',
 27+ 'set' => 'contestants',
 28+ );
 29+ }
 30+
 31+ public function __construct( $main, $action ) {
 32+ parent::__construct( $main, $action, 'ct' );
 33+ }
 34+
 35+ /**
 36+ * Handle the API request.
 37+ * Checks for access rights and then let's the parent method do the actual work.
 38+ */
 39+ public function execute() {
 40+ global $wgUser;
 41+
 42+ if ( !$wgUser->isAllowed( 'contestadmin' ) || $wgUser->isBlocked() ) {
 43+ $this->dieUsageMsg( array( 'badaccess-groups' ) );
 44+ }
 45+
 46+ parent::execute();
 47+ }
 48+
 49+ /**
 50+ * (non-PHPdoc)
 51+ * @see includes/api/ApiBase#getDescription()
 52+ */
 53+ public function getDescription() {
 54+ return 'API module for querying contestants';
 55+ }
 56+
 57+ /**
 58+ * (non-PHPdoc)
 59+ * @see includes/api/ApiBase#getExamples()
 60+ */
 61+ protected function getExamples() {
 62+ return array (
 63+ 'api.php?action=query&list=contestants&ctprops=id|user_id|contest_id|rating',
 64+ 'api.php?action=query&list=contestants&ctprops=id|rating&ctcontest_id=42',
 65+ );
 66+ }
 67+
 68+ /**
 69+ * (non-PHPdoc)
 70+ * @see includes/api/ApiBase#getVersion()
 71+ */
 72+ public function getVersion() {
 73+ return __CLASS__ . ': $Id$';
 74+ }
 75+
 76+}
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/api/ApiQueryContestants.php
___________________________________________________________________
Added: svn:keywords
177 + Id
Added: svn:eol-style
278 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/contest.special.submission.js
@@ -0,0 +1,19 @@
 2+/**
 3+ * JavasSript for the Contest MediaWiki extension.
 4+ * @see https://www.mediawiki.org/wiki/Extension:Contest
 5+ *
 6+ * @licence GNU GPL v3 or later
 7+ * @author Jeroen De Dauw <jeroendedauw at gmail dot com>
 8+ */
 9+
 10+(function( $, mw ) {
 11+
 12+ $( document ).ready( function() {
 13+
 14+ $( '.mw-htmlform-submit' ).button();
 15+
 16+ $( '.contest-submission' ).contestSubmission();
 17+
 18+ } );
 19+
 20+})( window.jQuery, window.mediaWiki );
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/contest.special.submission.js
___________________________________________________________________
Added: svn:eol-style
121 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/contest.special.welcome.js
@@ -0,0 +1,40 @@
 2+/**
 3+ * JavasSript for the Contest MediaWiki extension.
 4+ * @see https://www.mediawiki.org/wiki/Extension:Contest
 5+ *
 6+ * @licence GNU GPL v3 or later
 7+ * @author Jeroen De Dauw <jeroendedauw at gmail dot com>
 8+ */
 9+
 10+(function( $, mw ) {
 11+
 12+ $( document ).ready( function() {
 13+
 14+ $( '#contest-challenges' ).contestChallenges(
 15+ mw.config.get( 'ContestChallenges' ),
 16+ mw.config.get( 'ContestConfig' )
 17+ );
 18+
 19+ $rules = $( '#contest-rules' );
 20+
 21+ $div = $( '<div />' ).attr( {
 22+ 'style': 'display:none'
 23+ } ).html( $( '<div />' ).attr( { 'id': 'contest-rules-div' } ).html( $rules.attr( 'data-rules' ) ) );
 24+
 25+ // TODO: fix very ugly message construction.
 26+ $a = $( '<a />' ).text( mw.msg( 'contest-welcome-rules-link' ) ).attr( { 'href': '#contest-rules-div' } );
 27+ $p = $( '<p />' ).text( mw.msg( 'contest-welcome-rules' ) + ' ' ).append( $a ).append( '.' );
 28+
 29+ $rules.html( $p ).append( $div );
 30+
 31+ $a.fancybox( {
 32+ 'width' : '85%',
 33+ 'height' : '85%',
 34+ 'transitionIn' : 'none',
 35+ 'transitionOut' : 'none',
 36+ 'type' : 'inline',
 37+ 'autoDimensions': false
 38+ } );
 39+ } );
 40+
 41+})( window.jQuery, window.mediaWiki );
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/contest.special.welcome.js
___________________________________________________________________
Added: svn:eol-style
142 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/contest.contestant.pager.css
@@ -0,0 +1,11 @@
 2+/**
 3+ * CSS for the Contest MediaWiki extension.
 4+ * @see https://www.mediawiki.org/wiki/Extension:Contest
 5+ *
 6+ * @licence GNU GPL v3 or later
 7+ * @author Jeroen De Dauw <jeroendedauw at gmail dot com>
 8+ */
 9+
 10+.contestant-row td:hover {
 11+ cursor: pointer;
 12+}
\ No newline at end of file
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/contest.contestant.pager.css
___________________________________________________________________
Added: svn:eol-style
113 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/jquery.contestSubmission.js
@@ -0,0 +1,115 @@
 2+/**
 3+ * JavasSript for the Contest MediaWiki extension.
 4+ * @see https://www.mediawiki.org/wiki/Extension:Contest
 5+ *
 6+ * @licence GNU GPL v3 or later
 7+ * @author Jeroen De Dauw <jeroendedauw at gmail dot com>
 8+ */
 9+
 10+(function( $, mw ) {
 11+
 12+ /**
 13+ * Regex text escaping function.
 14+ * Borrowed from http://simonwillison.net/2006/Jan/20/escape/
 15+ */
 16+ RegExp.escape = function( text ) {
 17+ if ( !arguments.callee.sRE ) {
 18+ var specials = [ '/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\' ];
 19+ arguments.callee.sRE = new RegExp( '(\\' + specials.join('|\\') + ')', 'g' );
 20+ }
 21+ return text.replace(arguments.callee.sRE, '\\$1');
 22+ }
 23+
 24+ $.fn.contestSubmission = function() {
 25+ var _this = this;
 26+ var $this = $( this );
 27+
 28+ this.config = {};
 29+ this.status = {};
 30+
 31+ this.input = null;
 32+ this.label = null;
 33+
 34+ this.getValue = function() {
 35+ return this.input.val();
 36+ }
 37+
 38+ this.getDomains = function() {
 39+ return this.config.domains;
 40+ };
 41+
 42+ this.validate = function() {
 43+ var domains = _this.getDomains();
 44+
 45+ for ( var i = domains.length - 1; i >= 0; i-- ) {
 46+ var regex = new RegExp( "^https?://(([a-z0-9]+)\\.)?" + RegExp.escape( domains[i] ) + "/(.*)?$", "gi" );
 47+ if ( regex.test( this.getValue() ) ) {
 48+ return true;
 49+ }
 50+ }
 51+
 52+ return false;
 53+ };
 54+
 55+ this.showStatus = function() {
 56+ if ( _this.status.valid ) {
 57+ _this.input.removeClass( 'error' );
 58+ }
 59+ else {
 60+ _this.input.addClass( 'error' );
 61+ }
 62+ };
 63+
 64+ this.onValueChanged = function() {
 65+ _this.status.valid = _this.validate();
 66+ _this.showStatus();
 67+ };
 68+
 69+ this.setup = function() {
 70+ var message = $this.attr( 'data-value' ) === '' ? 'contest-submission-new-submission' : 'contest-submission-current-submission';
 71+ var domainLinks = [];
 72+
 73+ for ( var i = this.config.domains.length - 1; i >= 0; i-- ) {
 74+ var link = $( '<a />' ).text( this.config.domains[i] ).attr( {
 75+ 'href': 'http://' + this.config.domains[i],
 76+ 'target': 'blank'
 77+ } );
 78+ domainLinks.push( $( '<div />' ).html( link ).html() );
 79+ }
 80+
 81+ var links = $( '<span />' ).html( '' );
 82+
 83+ this.label = $( '<label style="display:block" />' ).attr( {
 84+ 'for': this.config.name,
 85+ } ).text( mw.msg( message ) ).append(
 86+ $( '<br />' ),
 87+ mw.msg( 'contest-submission-domains', domainLinks.join( ', ' ) )
 88+ );
 89+
 90+ this.input = $( '<input />' ).attr( {
 91+ 'type': 'text',
 92+ 'value': $this.attr( 'data-value' ),
 93+ 'name': this.config.name,
 94+ 'size': 45,
 95+ 'id': this.config.name
 96+ } );
 97+
 98+ this.html( this.label );
 99+ this.append( this.input );
 100+
 101+ this.input.keyup( this.onValueChanged );
 102+ };
 103+
 104+ this.getConfig = function() {
 105+ this.config.name = $this.attr( 'data-name' );
 106+ this.config.domains = $this.attr( 'data-domains' ).split( '|' );
 107+ };
 108+
 109+ this.getConfig();
 110+ this.setup();
 111+ this.onValueChanged();
 112+
 113+ return this;
 114+ };
 115+
 116+})( window.jQuery, window.mediaWiki );
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/jquery.contestSubmission.js
___________________________________________________________________
Added: svn:eol-style
1117 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/images/callout.png
Cannot display: file marked as a binary type.
svn:mime-type = image/png
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/images/callout.png
___________________________________________________________________
Added: svn:mime-type
2118 + image/png
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/images/arrow.png
Cannot display: file marked as a binary type.
svn:mime-type = image/png
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/images/arrow.png
___________________________________________________________________
Added: svn:mime-type
3119 + image/png
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/images/arrow-hover.png
Cannot display: file marked as a binary type.
svn:mime-type = image/png
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/images/arrow-hover.png
___________________________________________________________________
Added: svn:mime-type
4120 + image/png
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/images/box-sprite.png
Cannot display: file marked as a binary type.
svn:mime-type = image/png
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/images/box-sprite.png
___________________________________________________________________
Added: svn:mime-type
5121 + image/png
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/images/box-top-sprite.png
Cannot display: file marked as a binary type.
svn:mime-type = image/png
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/images/box-top-sprite.png
___________________________________________________________________
Added: svn:mime-type
6122 + image/png
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/images/box-bottom-sprite.png
Cannot display: file marked as a binary type.
svn:mime-type = image/png
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/images/box-bottom-sprite.png
___________________________________________________________________
Added: svn:mime-type
7123 + image/png
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/contest.special.contestant.css
@@ -0,0 +1,24 @@
 2+/**
 3+ * CSS for the Contest MediaWiki extension.
 4+ * @see https://www.mediawiki.org/wiki/Extension:Contest
 5+ *
 6+ * @licence GNU GPL v3 or later
 7+ * @author Jeroen De Dauw <jeroendedauw at gmail dot com>
 8+ */
 9+
 10+.contestant-comment {
 11+ border: solid 1px #aaaab3;
 12+ padding: 0;
 13+ background: #eee;
 14+ margin-bottom: 8px;
 15+ margin-left: 0px;
 16+}
 17+
 18+.contestant-comment-meta {
 19+ padding: 4px 8px;
 20+}
 21+
 22+.contestant-comment-text {
 23+ background: white;
 24+ padding: 16px;
 25+}
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/contest.special.contestant.css
___________________________________________________________________
Added: svn:eol-style
126 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/contest.contestant.pager.js
@@ -0,0 +1,19 @@
 2+/**
 3+ * JavasSript for the Contest MediaWiki extension.
 4+ * @see https://www.mediawiki.org/wiki/Extension:Contest
 5+ *
 6+ * @licence GNU GPL v3 or later
 7+ * @author Jeroen De Dauw <jeroendedauw at gmail dot com>
 8+ */
 9+
 10+(function( $, mw ) {
 11+
 12+ $( document ).ready( function() {
 13+
 14+ $( '.contestant-row' ).click( function() {
 15+ window.location = $( this ).attr( 'data-contestant-target' );
 16+ } );
 17+
 18+ } );
 19+
 20+})( window.jQuery, window.mediaWiki );
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/contest.contestant.pager.js
___________________________________________________________________
Added: svn:eol-style
121 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/fancy_shadow_sw.png
Cannot display: file marked as a binary type.
svn:mime-type = image/png
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/fancy_shadow_sw.png
___________________________________________________________________
Added: svn:mime-type
222 + image/png
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/fancy_nav_left.png
Cannot display: file marked as a binary type.
svn:mime-type = image/png
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/fancy_nav_left.png
___________________________________________________________________
Added: svn:mime-type
323 + image/png
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/fancybox-x.png
Cannot display: file marked as a binary type.
svn:mime-type = image/png
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/fancybox-x.png
___________________________________________________________________
Added: svn:mime-type
424 + image/png
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/fancybox-y.png
Cannot display: file marked as a binary type.
svn:mime-type = image/png
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/fancybox-y.png
___________________________________________________________________
Added: svn:mime-type
525 + image/png
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/jquery.fancybox-1.3.4.js
@@ -0,0 +1,1156 @@
 2+/*
 3+ * FancyBox - jQuery Plugin
 4+ * Simple and fancy lightbox alternative
 5+ *
 6+ * Examples and documentation at: http://fancybox.net
 7+ *
 8+ * Copyright (c) 2008 - 2010 Janis Skarnelis
 9+ * That said, it is hardly a one-person project. Many people have submitted bugs, code, and offered their advice freely. Their support is greatly appreciated.
 10+ *
 11+ * Version: 1.3.4 (11/11/2010)
 12+ * Requires: jQuery v1.3+
 13+ *
 14+ * Dual licensed under the MIT and GPL licenses:
 15+ * http://www.opensource.org/licenses/mit-license.php
 16+ * http://www.gnu.org/licenses/gpl.html
 17+ */
 18+
 19+;(function($) {
 20+ var tmp, loading, overlay, wrap, outer, content, close, title, nav_left, nav_right,
 21+
 22+ selectedIndex = 0, selectedOpts = {}, selectedArray = [], currentIndex = 0, currentOpts = {}, currentArray = [],
 23+
 24+ ajaxLoader = null, imgPreloader = new Image(), imgRegExp = /\.(jpg|gif|png|bmp|jpeg)(.*)?$/i, swfRegExp = /[^\.]\.(swf)\s*$/i,
 25+
 26+ loadingTimer, loadingFrame = 1,
 27+
 28+ titleHeight = 0, titleStr = '', start_pos, final_pos, busy = false, fx = $.extend($('<div/>')[0], { prop: 0 }),
 29+
 30+ isIE6 = $.browser.msie && $.browser.version < 7 && !window.XMLHttpRequest,
 31+
 32+ /*
 33+ * Private methods
 34+ */
 35+
 36+ _abort = function() {
 37+ loading.hide();
 38+
 39+ imgPreloader.onerror = imgPreloader.onload = null;
 40+
 41+ if (ajaxLoader) {
 42+ ajaxLoader.abort();
 43+ }
 44+
 45+ tmp.empty();
 46+ },
 47+
 48+ _error = function() {
 49+ if (false === selectedOpts.onError(selectedArray, selectedIndex, selectedOpts)) {
 50+ loading.hide();
 51+ busy = false;
 52+ return;
 53+ }
 54+
 55+ selectedOpts.titleShow = false;
 56+
 57+ selectedOpts.width = 'auto';
 58+ selectedOpts.height = 'auto';
 59+
 60+ tmp.html( '<p id="fancybox-error">The requested content cannot be loaded.<br />Please try again later.</p>' );
 61+
 62+ _process_inline();
 63+ },
 64+
 65+ _start = function() {
 66+ var obj = selectedArray[ selectedIndex ],
 67+ href,
 68+ type,
 69+ title,
 70+ str,
 71+ emb,
 72+ ret;
 73+
 74+ _abort();
 75+
 76+ selectedOpts = $.extend({}, $.fn.fancybox.defaults, (typeof $(obj).data('fancybox') == 'undefined' ? selectedOpts : $(obj).data('fancybox')));
 77+
 78+ ret = selectedOpts.onStart(selectedArray, selectedIndex, selectedOpts);
 79+
 80+ if (ret === false) {
 81+ busy = false;
 82+ return;
 83+ } else if (typeof ret == 'object') {
 84+ selectedOpts = $.extend(selectedOpts, ret);
 85+ }
 86+
 87+ title = selectedOpts.title || (obj.nodeName ? $(obj).attr('title') : obj.title) || '';
 88+
 89+ if (obj.nodeName && !selectedOpts.orig) {
 90+ selectedOpts.orig = $(obj).children("img:first").length ? $(obj).children("img:first") : $(obj);
 91+ }
 92+
 93+ if (title === '' && selectedOpts.orig && selectedOpts.titleFromAlt) {
 94+ title = selectedOpts.orig.attr('alt');
 95+ }
 96+
 97+ href = selectedOpts.href || (obj.nodeName ? $(obj).attr('href') : obj.href) || null;
 98+
 99+ if ((/^(?:javascript)/i).test(href) || href == '#') {
 100+ href = null;
 101+ }
 102+
 103+ if (selectedOpts.type) {
 104+ type = selectedOpts.type;
 105+
 106+ if (!href) {
 107+ href = selectedOpts.content;
 108+ }
 109+
 110+ } else if (selectedOpts.content) {
 111+ type = 'html';
 112+
 113+ } else if (href) {
 114+ if (href.match(imgRegExp)) {
 115+ type = 'image';
 116+
 117+ } else if (href.match(swfRegExp)) {
 118+ type = 'swf';
 119+
 120+ } else if ($(obj).hasClass("iframe")) {
 121+ type = 'iframe';
 122+
 123+ } else if (href.indexOf("#") === 0) {
 124+ type = 'inline';
 125+
 126+ } else {
 127+ type = 'ajax';
 128+ }
 129+ }
 130+
 131+ if (!type) {
 132+ _error();
 133+ return;
 134+ }
 135+
 136+ if (type == 'inline') {
 137+ obj = href.substr(href.indexOf("#"));
 138+ type = $(obj).length > 0 ? 'inline' : 'ajax';
 139+ }
 140+
 141+ selectedOpts.type = type;
 142+ selectedOpts.href = href;
 143+ selectedOpts.title = title;
 144+
 145+ if (selectedOpts.autoDimensions) {
 146+ if (selectedOpts.type == 'html' || selectedOpts.type == 'inline' || selectedOpts.type == 'ajax') {
 147+ selectedOpts.width = 'auto';
 148+ selectedOpts.height = 'auto';
 149+ } else {
 150+ selectedOpts.autoDimensions = false;
 151+ }
 152+ }
 153+
 154+ if (selectedOpts.modal) {
 155+ selectedOpts.overlayShow = true;
 156+ selectedOpts.hideOnOverlayClick = false;
 157+ selectedOpts.hideOnContentClick = false;
 158+ selectedOpts.enableEscapeButton = false;
 159+ selectedOpts.showCloseButton = false;
 160+ }
 161+
 162+ selectedOpts.padding = parseInt(selectedOpts.padding, 10);
 163+ selectedOpts.margin = parseInt(selectedOpts.margin, 10);
 164+
 165+ tmp.css('padding', (selectedOpts.padding + selectedOpts.margin));
 166+
 167+ $('.fancybox-inline-tmp').unbind('fancybox-cancel').bind('fancybox-change', function() {
 168+ $(this).replaceWith(content.children());
 169+ });
 170+
 171+ switch (type) {
 172+ case 'html' :
 173+ tmp.html( selectedOpts.content );
 174+ _process_inline();
 175+ break;
 176+
 177+ case 'inline' :
 178+ if ( $(obj).parent().is('#fancybox-content') === true) {
 179+ busy = false;
 180+ return;
 181+ }
 182+
 183+ $('<div class="fancybox-inline-tmp" />')
 184+ .hide()
 185+ .insertBefore( $(obj) )
 186+ .bind('fancybox-cleanup', function() {
 187+ $(this).replaceWith(content.children());
 188+ }).bind('fancybox-cancel', function() {
 189+ $(this).replaceWith(tmp.children());
 190+ });
 191+
 192+ $(obj).appendTo(tmp);
 193+
 194+ _process_inline();
 195+ break;
 196+
 197+ case 'image':
 198+ busy = false;
 199+
 200+ $.fancybox.showActivity();
 201+
 202+ imgPreloader = new Image();
 203+
 204+ imgPreloader.onerror = function() {
 205+ _error();
 206+ };
 207+
 208+ imgPreloader.onload = function() {
 209+ busy = true;
 210+
 211+ imgPreloader.onerror = imgPreloader.onload = null;
 212+
 213+ _process_image();
 214+ };
 215+
 216+ imgPreloader.src = href;
 217+ break;
 218+
 219+ case 'swf':
 220+ selectedOpts.scrolling = 'no';
 221+
 222+ str = '<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" width="' + selectedOpts.width + '" height="' + selectedOpts.height + '"><param name="movie" value="' + href + '"></param>';
 223+ emb = '';
 224+
 225+ $.each(selectedOpts.swf, function(name, val) {
 226+ str += '<param name="' + name + '" value="' + val + '"></param>';
 227+ emb += ' ' + name + '="' + val + '"';
 228+ });
 229+
 230+ str += '<embed src="' + href + '" type="application/x-shockwave-flash" width="' + selectedOpts.width + '" height="' + selectedOpts.height + '"' + emb + '></embed></object>';
 231+
 232+ tmp.html(str);
 233+
 234+ _process_inline();
 235+ break;
 236+
 237+ case 'ajax':
 238+ busy = false;
 239+
 240+ $.fancybox.showActivity();
 241+
 242+ selectedOpts.ajax.win = selectedOpts.ajax.success;
 243+
 244+ ajaxLoader = $.ajax($.extend({}, selectedOpts.ajax, {
 245+ url : href,
 246+ data : selectedOpts.ajax.data || {},
 247+ error : function(XMLHttpRequest, textStatus, errorThrown) {
 248+ if ( XMLHttpRequest.status > 0 ) {
 249+ _error();
 250+ }
 251+ },
 252+ success : function(data, textStatus, XMLHttpRequest) {
 253+ var o = typeof XMLHttpRequest == 'object' ? XMLHttpRequest : ajaxLoader;
 254+ if (o.status == 200) {
 255+ if ( typeof selectedOpts.ajax.win == 'function' ) {
 256+ ret = selectedOpts.ajax.win(href, data, textStatus, XMLHttpRequest);
 257+
 258+ if (ret === false) {
 259+ loading.hide();
 260+ return;
 261+ } else if (typeof ret == 'string' || typeof ret == 'object') {
 262+ data = ret;
 263+ }
 264+ }
 265+
 266+ tmp.html( data );
 267+ _process_inline();
 268+ }
 269+ }
 270+ }));
 271+
 272+ break;
 273+
 274+ case 'iframe':
 275+ _show();
 276+ break;
 277+ }
 278+ },
 279+
 280+ _process_inline = function() {
 281+ var
 282+ w = selectedOpts.width,
 283+ h = selectedOpts.height;
 284+
 285+ if (w.toString().indexOf('%') > -1) {
 286+ w = parseInt( ($(window).width() - (selectedOpts.margin * 2)) * parseFloat(w) / 100, 10) + 'px';
 287+
 288+ } else {
 289+ w = w == 'auto' ? 'auto' : w + 'px';
 290+ }
 291+
 292+ if (h.toString().indexOf('%') > -1) {
 293+ h = parseInt( ($(window).height() - (selectedOpts.margin * 2)) * parseFloat(h) / 100, 10) + 'px';
 294+
 295+ } else {
 296+ h = h == 'auto' ? 'auto' : h + 'px';
 297+ }
 298+
 299+ tmp.wrapInner('<div style="width:' + w + ';height:' + h + ';overflow: ' + (selectedOpts.scrolling == 'auto' ? 'auto' : (selectedOpts.scrolling == 'yes' ? 'scroll' : 'hidden')) + ';position:relative;"></div>');
 300+
 301+ selectedOpts.width = tmp.width();
 302+ selectedOpts.height = tmp.height();
 303+
 304+ _show();
 305+ },
 306+
 307+ _process_image = function() {
 308+ selectedOpts.width = imgPreloader.width;
 309+ selectedOpts.height = imgPreloader.height;
 310+
 311+ $("<img />").attr({
 312+ 'id' : 'fancybox-img',
 313+ 'src' : imgPreloader.src,
 314+ 'alt' : selectedOpts.title
 315+ }).appendTo( tmp );
 316+
 317+ _show();
 318+ },
 319+
 320+ _show = function() {
 321+ var pos, equal;
 322+
 323+ loading.hide();
 324+
 325+ if (wrap.is(":visible") && false === currentOpts.onCleanup(currentArray, currentIndex, currentOpts)) {
 326+ $.event.trigger('fancybox-cancel');
 327+
 328+ busy = false;
 329+ return;
 330+ }
 331+
 332+ busy = true;
 333+
 334+ $(content.add( overlay )).unbind();
 335+
 336+ $(window).unbind("resize.fb scroll.fb");
 337+ $(document).unbind('keydown.fb');
 338+
 339+ if (wrap.is(":visible") && currentOpts.titlePosition !== 'outside') {
 340+ wrap.css('height', wrap.height());
 341+ }
 342+
 343+ currentArray = selectedArray;
 344+ currentIndex = selectedIndex;
 345+ currentOpts = selectedOpts;
 346+
 347+ if (currentOpts.overlayShow) {
 348+ overlay.css({
 349+ 'background-color' : currentOpts.overlayColor,
 350+ 'opacity' : currentOpts.overlayOpacity,
 351+ 'cursor' : currentOpts.hideOnOverlayClick ? 'pointer' : 'auto',
 352+ 'height' : $(document).height()
 353+ });
 354+
 355+ if (!overlay.is(':visible')) {
 356+ if (isIE6) {
 357+ $('select:not(#fancybox-tmp select)').filter(function() {
 358+ return this.style.visibility !== 'hidden';
 359+ }).css({'visibility' : 'hidden'}).one('fancybox-cleanup', function() {
 360+ this.style.visibility = 'inherit';
 361+ });
 362+ }
 363+
 364+ overlay.show();
 365+ }
 366+ } else {
 367+ overlay.hide();
 368+ }
 369+
 370+ final_pos = _get_zoom_to();
 371+
 372+ _process_title();
 373+
 374+ if (wrap.is(":visible")) {
 375+ $( close.add( nav_left ).add( nav_right ) ).hide();
 376+
 377+ pos = wrap.position(),
 378+
 379+ start_pos = {
 380+ top : pos.top,
 381+ left : pos.left,
 382+ width : wrap.width(),
 383+ height : wrap.height()
 384+ };
 385+
 386+ equal = (start_pos.width == final_pos.width && start_pos.height == final_pos.height);
 387+
 388+ content.fadeTo(currentOpts.changeFade, 0.3, function() {
 389+ var finish_resizing = function() {
 390+ content.html( tmp.contents() ).fadeTo(currentOpts.changeFade, 1, _finish);
 391+ };
 392+
 393+ $.event.trigger('fancybox-change');
 394+
 395+ content
 396+ .empty()
 397+ .removeAttr('filter')
 398+ .css({
 399+ 'border-width' : currentOpts.padding,
 400+ 'width' : final_pos.width - currentOpts.padding * 2,
 401+ 'height' : selectedOpts.autoDimensions ? 'auto' : final_pos.height - titleHeight - currentOpts.padding * 2
 402+ });
 403+
 404+ if (equal) {
 405+ finish_resizing();
 406+
 407+ } else {
 408+ fx.prop = 0;
 409+
 410+ $(fx).animate({prop: 1}, {
 411+ duration : currentOpts.changeSpeed,
 412+ easing : currentOpts.easingChange,
 413+ step : _draw,
 414+ complete : finish_resizing
 415+ });
 416+ }
 417+ });
 418+
 419+ return;
 420+ }
 421+
 422+ wrap.removeAttr("style");
 423+
 424+ content.css('border-width', currentOpts.padding);
 425+
 426+ if (currentOpts.transitionIn == 'elastic') {
 427+ start_pos = _get_zoom_from();
 428+
 429+ content.html( tmp.contents() );
 430+
 431+ wrap.show();
 432+
 433+ if (currentOpts.opacity) {
 434+ final_pos.opacity = 0;
 435+ }
 436+
 437+ fx.prop = 0;
 438+
 439+ $(fx).animate({prop: 1}, {
 440+ duration : currentOpts.speedIn,
 441+ easing : currentOpts.easingIn,
 442+ step : _draw,
 443+ complete : _finish
 444+ });
 445+
 446+ return;
 447+ }
 448+
 449+ if (currentOpts.titlePosition == 'inside' && titleHeight > 0) {
 450+ title.show();
 451+ }
 452+
 453+ content
 454+ .css({
 455+ 'width' : final_pos.width - currentOpts.padding * 2,
 456+ 'height' : selectedOpts.autoDimensions ? 'auto' : final_pos.height - titleHeight - currentOpts.padding * 2
 457+ })
 458+ .html( tmp.contents() );
 459+
 460+ wrap
 461+ .css(final_pos)
 462+ .fadeIn( currentOpts.transitionIn == 'none' ? 0 : currentOpts.speedIn, _finish );
 463+ },
 464+
 465+ _format_title = function(title) {
 466+ if (title && title.length) {
 467+ if (currentOpts.titlePosition == 'float') {
 468+ return '<table id="fancybox-title-float-wrap" cellpadding="0" cellspacing="0"><tr><td id="fancybox-title-float-left"></td><td id="fancybox-title-float-main">' + title + '</td><td id="fancybox-title-float-right"></td></tr></table>';
 469+ }
 470+
 471+ return '<div id="fancybox-title-' + currentOpts.titlePosition + '">' + title + '</div>';
 472+ }
 473+
 474+ return false;
 475+ },
 476+
 477+ _process_title = function() {
 478+ titleStr = currentOpts.title || '';
 479+ titleHeight = 0;
 480+
 481+ title
 482+ .empty()
 483+ .removeAttr('style')
 484+ .removeClass();
 485+
 486+ if (currentOpts.titleShow === false) {
 487+ title.hide();
 488+ return;
 489+ }
 490+
 491+ titleStr = $.isFunction(currentOpts.titleFormat) ? currentOpts.titleFormat(titleStr, currentArray, currentIndex, currentOpts) : _format_title(titleStr);
 492+
 493+ if (!titleStr || titleStr === '') {
 494+ title.hide();
 495+ return;
 496+ }
 497+
 498+ title
 499+ .addClass('fancybox-title-' + currentOpts.titlePosition)
 500+ .html( titleStr )
 501+ .appendTo( 'body' )
 502+ .show();
 503+
 504+ switch (currentOpts.titlePosition) {
 505+ case 'inside':
 506+ title
 507+ .css({
 508+ 'width' : final_pos.width - (currentOpts.padding * 2),
 509+ 'marginLeft' : currentOpts.padding,
 510+ 'marginRight' : currentOpts.padding
 511+ });
 512+
 513+ titleHeight = title.outerHeight(true);
 514+
 515+ title.appendTo( outer );
 516+
 517+ final_pos.height += titleHeight;
 518+ break;
 519+
 520+ case 'over':
 521+ title
 522+ .css({
 523+ 'marginLeft' : currentOpts.padding,
 524+ 'width' : final_pos.width - (currentOpts.padding * 2),
 525+ 'bottom' : currentOpts.padding
 526+ })
 527+ .appendTo( outer );
 528+ break;
 529+
 530+ case 'float':
 531+ title
 532+ .css('left', parseInt((title.width() - final_pos.width - 40)/ 2, 10) * -1)
 533+ .appendTo( wrap );
 534+ break;
 535+
 536+ default:
 537+ title
 538+ .css({
 539+ 'width' : final_pos.width - (currentOpts.padding * 2),
 540+ 'paddingLeft' : currentOpts.padding,
 541+ 'paddingRight' : currentOpts.padding
 542+ })
 543+ .appendTo( wrap );
 544+ break;
 545+ }
 546+
 547+ title.hide();
 548+ },
 549+
 550+ _set_navigation = function() {
 551+ if (currentOpts.enableEscapeButton || currentOpts.enableKeyboardNav) {
 552+ $(document).bind('keydown.fb', function(e) {
 553+ if (e.keyCode == 27 && currentOpts.enableEscapeButton) {
 554+ e.preventDefault();
 555+ $.fancybox.close();
 556+
 557+ } else if ((e.keyCode == 37 || e.keyCode == 39) && currentOpts.enableKeyboardNav && e.target.tagName !== 'INPUT' && e.target.tagName !== 'TEXTAREA' && e.target.tagName !== 'SELECT') {
 558+ e.preventDefault();
 559+ $.fancybox[ e.keyCode == 37 ? 'prev' : 'next']();
 560+ }
 561+ });
 562+ }
 563+
 564+ if (!currentOpts.showNavArrows) {
 565+ nav_left.hide();
 566+ nav_right.hide();
 567+ return;
 568+ }
 569+
 570+ if ((currentOpts.cyclic && currentArray.length > 1) || currentIndex !== 0) {
 571+ nav_left.show();
 572+ }
 573+
 574+ if ((currentOpts.cyclic && currentArray.length > 1) || currentIndex != (currentArray.length -1)) {
 575+ nav_right.show();
 576+ }
 577+ },
 578+
 579+ _finish = function () {
 580+ if (!$.support.opacity) {
 581+ content.get(0).style.removeAttribute('filter');
 582+ wrap.get(0).style.removeAttribute('filter');
 583+ }
 584+
 585+ if (selectedOpts.autoDimensions) {
 586+ content.css('height', 'auto');
 587+ }
 588+
 589+ wrap.css('height', 'auto');
 590+
 591+ if (titleStr && titleStr.length) {
 592+ title.show();
 593+ }
 594+
 595+ if (currentOpts.showCloseButton) {
 596+ close.show();
 597+ }
 598+
 599+ _set_navigation();
 600+
 601+ if (currentOpts.hideOnContentClick) {
 602+ content.bind('click', $.fancybox.close);
 603+ }
 604+
 605+ if (currentOpts.hideOnOverlayClick) {
 606+ overlay.bind('click', $.fancybox.close);
 607+ }
 608+
 609+ $(window).bind("resize.fb", $.fancybox.resize);
 610+
 611+ if (currentOpts.centerOnScroll) {
 612+ $(window).bind("scroll.fb", $.fancybox.center);
 613+ }
 614+
 615+ if (currentOpts.type == 'iframe') {
 616+ $('<iframe id="fancybox-frame" name="fancybox-frame' + new Date().getTime() + '" frameborder="0" hspace="0" ' + ($.browser.msie ? 'allowtransparency="true""' : '') + ' scrolling="' + selectedOpts.scrolling + '" src="' + currentOpts.href + '"></iframe>').appendTo(content);
 617+ }
 618+
 619+ wrap.show();
 620+
 621+ busy = false;
 622+
 623+ $.fancybox.center();
 624+
 625+ currentOpts.onComplete(currentArray, currentIndex, currentOpts);
 626+
 627+ _preload_images();
 628+ },
 629+
 630+ _preload_images = function() {
 631+ var href,
 632+ objNext;
 633+
 634+ if ((currentArray.length -1) > currentIndex) {
 635+ href = currentArray[ currentIndex + 1 ].href;
 636+
 637+ if (typeof href !== 'undefined' && href.match(imgRegExp)) {
 638+ objNext = new Image();
 639+ objNext.src = href;
 640+ }
 641+ }
 642+
 643+ if (currentIndex > 0) {
 644+ href = currentArray[ currentIndex - 1 ].href;
 645+
 646+ if (typeof href !== 'undefined' && href.match(imgRegExp)) {
 647+ objNext = new Image();
 648+ objNext.src = href;
 649+ }
 650+ }
 651+ },
 652+
 653+ _draw = function(pos) {
 654+ var dim = {
 655+ width : parseInt(start_pos.width + (final_pos.width - start_pos.width) * pos, 10),
 656+ height : parseInt(start_pos.height + (final_pos.height - start_pos.height) * pos, 10),
 657+
 658+ top : parseInt(start_pos.top + (final_pos.top - start_pos.top) * pos, 10),
 659+ left : parseInt(start_pos.left + (final_pos.left - start_pos.left) * pos, 10)
 660+ };
 661+
 662+ if (typeof final_pos.opacity !== 'undefined') {
 663+ dim.opacity = pos < 0.5 ? 0.5 : pos;
 664+ }
 665+
 666+ wrap.css(dim);
 667+
 668+ content.css({
 669+ 'width' : dim.width - currentOpts.padding * 2,
 670+ 'height' : dim.height - (titleHeight * pos) - currentOpts.padding * 2
 671+ });
 672+ },
 673+
 674+ _get_viewport = function() {
 675+ return [
 676+ $(window).width() - (currentOpts.margin * 2),
 677+ $(window).height() - (currentOpts.margin * 2),
 678+ $(document).scrollLeft() + currentOpts.margin,
 679+ $(document).scrollTop() + currentOpts.margin
 680+ ];
 681+ },
 682+
 683+ _get_zoom_to = function () {
 684+ var view = _get_viewport(),
 685+ to = {},
 686+ resize = currentOpts.autoScale,
 687+ double_padding = currentOpts.padding * 2,
 688+ ratio;
 689+
 690+ if (currentOpts.width.toString().indexOf('%') > -1) {
 691+ to.width = parseInt((view[0] * parseFloat(currentOpts.width)) / 100, 10);
 692+ } else {
 693+ to.width = currentOpts.width + double_padding;
 694+ }
 695+
 696+ if (currentOpts.height.toString().indexOf('%') > -1) {
 697+ to.height = parseInt((view[1] * parseFloat(currentOpts.height)) / 100, 10);
 698+ } else {
 699+ to.height = currentOpts.height + double_padding;
 700+ }
 701+
 702+ if (resize && (to.width > view[0] || to.height > view[1])) {
 703+ if (selectedOpts.type == 'image' || selectedOpts.type == 'swf') {
 704+ ratio = (currentOpts.width ) / (currentOpts.height );
 705+
 706+ if ((to.width ) > view[0]) {
 707+ to.width = view[0];
 708+ to.height = parseInt(((to.width - double_padding) / ratio) + double_padding, 10);
 709+ }
 710+
 711+ if ((to.height) > view[1]) {
 712+ to.height = view[1];
 713+ to.width = parseInt(((to.height - double_padding) * ratio) + double_padding, 10);
 714+ }
 715+
 716+ } else {
 717+ to.width = Math.min(to.width, view[0]);
 718+ to.height = Math.min(to.height, view[1]);
 719+ }
 720+ }
 721+
 722+ to.top = parseInt(Math.max(view[3] - 20, view[3] + ((view[1] - to.height - 40) * 0.5)), 10);
 723+ to.left = parseInt(Math.max(view[2] - 20, view[2] + ((view[0] - to.width - 40) * 0.5)), 10);
 724+
 725+ return to;
 726+ },
 727+
 728+ _get_obj_pos = function(obj) {
 729+ var pos = obj.offset();
 730+
 731+ pos.top += parseInt( obj.css('paddingTop'), 10 ) || 0;
 732+ pos.left += parseInt( obj.css('paddingLeft'), 10 ) || 0;
 733+
 734+ pos.top += parseInt( obj.css('border-top-width'), 10 ) || 0;
 735+ pos.left += parseInt( obj.css('border-left-width'), 10 ) || 0;
 736+
 737+ pos.width = obj.width();
 738+ pos.height = obj.height();
 739+
 740+ return pos;
 741+ },
 742+
 743+ _get_zoom_from = function() {
 744+ var orig = selectedOpts.orig ? $(selectedOpts.orig) : false,
 745+ from = {},
 746+ pos,
 747+ view;
 748+
 749+ if (orig && orig.length) {
 750+ pos = _get_obj_pos(orig);
 751+
 752+ from = {
 753+ width : pos.width + (currentOpts.padding * 2),
 754+ height : pos.height + (currentOpts.padding * 2),
 755+ top : pos.top - currentOpts.padding - 20,
 756+ left : pos.left - currentOpts.padding - 20
 757+ };
 758+
 759+ } else {
 760+ view = _get_viewport();
 761+
 762+ from = {
 763+ width : currentOpts.padding * 2,
 764+ height : currentOpts.padding * 2,
 765+ top : parseInt(view[3] + view[1] * 0.5, 10),
 766+ left : parseInt(view[2] + view[0] * 0.5, 10)
 767+ };
 768+ }
 769+
 770+ return from;
 771+ },
 772+
 773+ _animate_loading = function() {
 774+ if (!loading.is(':visible')){
 775+ clearInterval(loadingTimer);
 776+ return;
 777+ }
 778+
 779+ $('div', loading).css('top', (loadingFrame * -40) + 'px');
 780+
 781+ loadingFrame = (loadingFrame + 1) % 12;
 782+ };
 783+
 784+ /*
 785+ * Public methods
 786+ */
 787+
 788+ $.fn.fancybox = function(options) {
 789+ if (!$(this).length) {
 790+ return this;
 791+ }
 792+
 793+ $(this)
 794+ .data('fancybox', $.extend({}, options, ($.metadata ? $(this).metadata() : {})))
 795+ .unbind('click.fb')
 796+ .bind('click.fb', function(e) {
 797+ e.preventDefault();
 798+
 799+ if (busy) {
 800+ return;
 801+ }
 802+
 803+ busy = true;
 804+
 805+ $(this).blur();
 806+
 807+ selectedArray = [];
 808+ selectedIndex = 0;
 809+
 810+ var rel = $(this).attr('rel') || '';
 811+
 812+ if (!rel || rel == '' || rel === 'nofollow') {
 813+ selectedArray.push(this);
 814+
 815+ } else {
 816+ selectedArray = $("a[rel=" + rel + "], area[rel=" + rel + "]");
 817+ selectedIndex = selectedArray.index( this );
 818+ }
 819+
 820+ _start();
 821+
 822+ return;
 823+ });
 824+
 825+ return this;
 826+ };
 827+
 828+ $.fancybox = function(obj) {
 829+ var opts;
 830+
 831+ if (busy) {
 832+ return;
 833+ }
 834+
 835+ busy = true;
 836+ opts = typeof arguments[1] !== 'undefined' ? arguments[1] : {};
 837+
 838+ selectedArray = [];
 839+ selectedIndex = parseInt(opts.index, 10) || 0;
 840+
 841+ if ($.isArray(obj)) {
 842+ for (var i = 0, j = obj.length; i < j; i++) {
 843+ if (typeof obj[i] == 'object') {
 844+ $(obj[i]).data('fancybox', $.extend({}, opts, obj[i]));
 845+ } else {
 846+ obj[i] = $({}).data('fancybox', $.extend({content : obj[i]}, opts));
 847+ }
 848+ }
 849+
 850+ selectedArray = jQuery.merge(selectedArray, obj);
 851+
 852+ } else {
 853+ if (typeof obj == 'object') {
 854+ $(obj).data('fancybox', $.extend({}, opts, obj));
 855+ } else {
 856+ obj = $({}).data('fancybox', $.extend({content : obj}, opts));
 857+ }
 858+
 859+ selectedArray.push(obj);
 860+ }
 861+
 862+ if (selectedIndex > selectedArray.length || selectedIndex < 0) {
 863+ selectedIndex = 0;
 864+ }
 865+
 866+ _start();
 867+ };
 868+
 869+ $.fancybox.showActivity = function() {
 870+ clearInterval(loadingTimer);
 871+
 872+ loading.show();
 873+ loadingTimer = setInterval(_animate_loading, 66);
 874+ };
 875+
 876+ $.fancybox.hideActivity = function() {
 877+ loading.hide();
 878+ };
 879+
 880+ $.fancybox.next = function() {
 881+ return $.fancybox.pos( currentIndex + 1);
 882+ };
 883+
 884+ $.fancybox.prev = function() {
 885+ return $.fancybox.pos( currentIndex - 1);
 886+ };
 887+
 888+ $.fancybox.pos = function(pos) {
 889+ if (busy) {
 890+ return;
 891+ }
 892+
 893+ pos = parseInt(pos);
 894+
 895+ selectedArray = currentArray;
 896+
 897+ if (pos > -1 && pos < currentArray.length) {
 898+ selectedIndex = pos;
 899+ _start();
 900+
 901+ } else if (currentOpts.cyclic && currentArray.length > 1) {
 902+ selectedIndex = pos >= currentArray.length ? 0 : currentArray.length - 1;
 903+ _start();
 904+ }
 905+
 906+ return;
 907+ };
 908+
 909+ $.fancybox.cancel = function() {
 910+ if (busy) {
 911+ return;
 912+ }
 913+
 914+ busy = true;
 915+
 916+ $.event.trigger('fancybox-cancel');
 917+
 918+ _abort();
 919+
 920+ selectedOpts.onCancel(selectedArray, selectedIndex, selectedOpts);
 921+
 922+ busy = false;
 923+ };
 924+
 925+ // Note: within an iframe use - parent.$.fancybox.close();
 926+ $.fancybox.close = function() {
 927+ if (busy || wrap.is(':hidden')) {
 928+ return;
 929+ }
 930+
 931+ busy = true;
 932+
 933+ if (currentOpts && false === currentOpts.onCleanup(currentArray, currentIndex, currentOpts)) {
 934+ busy = false;
 935+ return;
 936+ }
 937+
 938+ _abort();
 939+
 940+ $(close.add( nav_left ).add( nav_right )).hide();
 941+
 942+ $(content.add( overlay )).unbind();
 943+
 944+ $(window).unbind("resize.fb scroll.fb");
 945+ $(document).unbind('keydown.fb');
 946+
 947+ content.find('iframe').attr('src', isIE6 && /^https/i.test(window.location.href || '') ? 'javascript:void(false)' : 'about:blank');
 948+
 949+ if (currentOpts.titlePosition !== 'inside') {
 950+ title.empty();
 951+ }
 952+
 953+ wrap.stop();
 954+
 955+ function _cleanup() {
 956+ overlay.fadeOut('fast');
 957+
 958+ title.empty().hide();
 959+ wrap.hide();
 960+
 961+ $.event.trigger('fancybox-cleanup');
 962+
 963+ content.empty();
 964+
 965+ currentOpts.onClosed(currentArray, currentIndex, currentOpts);
 966+
 967+ currentArray = selectedOpts = [];
 968+ currentIndex = selectedIndex = 0;
 969+ currentOpts = selectedOpts = {};
 970+
 971+ busy = false;
 972+ }
 973+
 974+ if (currentOpts.transitionOut == 'elastic') {
 975+ start_pos = _get_zoom_from();
 976+
 977+ var pos = wrap.position();
 978+
 979+ final_pos = {
 980+ top : pos.top ,
 981+ left : pos.left,
 982+ width : wrap.width(),
 983+ height : wrap.height()
 984+ };
 985+
 986+ if (currentOpts.opacity) {
 987+ final_pos.opacity = 1;
 988+ }
 989+
 990+ title.empty().hide();
 991+
 992+ fx.prop = 1;
 993+
 994+ $(fx).animate({ prop: 0 }, {
 995+ duration : currentOpts.speedOut,
 996+ easing : currentOpts.easingOut,
 997+ step : _draw,
 998+ complete : _cleanup
 999+ });
 1000+
 1001+ } else {
 1002+ wrap.fadeOut( currentOpts.transitionOut == 'none' ? 0 : currentOpts.speedOut, _cleanup);
 1003+ }
 1004+ };
 1005+
 1006+ $.fancybox.resize = function() {
 1007+ if (overlay.is(':visible')) {
 1008+ overlay.css('height', $(document).height());
 1009+ }
 1010+
 1011+ $.fancybox.center(true);
 1012+ };
 1013+
 1014+ $.fancybox.center = function() {
 1015+ var view, align;
 1016+
 1017+ if (busy) {
 1018+ return;
 1019+ }
 1020+
 1021+ align = arguments[0] === true ? 1 : 0;
 1022+ view = _get_viewport();
 1023+
 1024+ if (!align && (wrap.width() > view[0] || wrap.height() > view[1])) {
 1025+ return;
 1026+ }
 1027+
 1028+ wrap
 1029+ .stop()
 1030+ .animate({
 1031+ 'top' : parseInt(Math.max(view[3] - 20, view[3] + ((view[1] - content.height() - 40) * 0.5) - currentOpts.padding)),
 1032+ 'left' : parseInt(Math.max(view[2] - 20, view[2] + ((view[0] - content.width() - 40) * 0.5) - currentOpts.padding))
 1033+ }, typeof arguments[0] == 'number' ? arguments[0] : 200);
 1034+ };
 1035+
 1036+ $.fancybox.init = function() {
 1037+ if ($("#fancybox-wrap").length) {
 1038+ return;
 1039+ }
 1040+
 1041+ $('body').append(
 1042+ tmp = $('<div id="fancybox-tmp"></div>'),
 1043+ loading = $('<div id="fancybox-loading"><div></div></div>'),
 1044+ overlay = $('<div id="fancybox-overlay"></div>'),
 1045+ wrap = $('<div id="fancybox-wrap"></div>')
 1046+ );
 1047+
 1048+ outer = $('<div id="fancybox-outer"></div>')
 1049+ .append('<div class="fancybox-bg" id="fancybox-bg-n"></div><div class="fancybox-bg" id="fancybox-bg-ne"></div><div class="fancybox-bg" id="fancybox-bg-e"></div><div class="fancybox-bg" id="fancybox-bg-se"></div><div class="fancybox-bg" id="fancybox-bg-s"></div><div class="fancybox-bg" id="fancybox-bg-sw"></div><div class="fancybox-bg" id="fancybox-bg-w"></div><div class="fancybox-bg" id="fancybox-bg-nw"></div>')
 1050+ .appendTo( wrap );
 1051+
 1052+ outer.append(
 1053+ content = $('<div id="fancybox-content"></div>'),
 1054+ close = $('<a id="fancybox-close"></a>'),
 1055+ title = $('<div id="fancybox-title"></div>'),
 1056+
 1057+ nav_left = $('<a href="javascript:;" id="fancybox-left"><span class="fancy-ico" id="fancybox-left-ico"></span></a>'),
 1058+ nav_right = $('<a href="javascript:;" id="fancybox-right"><span class="fancy-ico" id="fancybox-right-ico"></span></a>')
 1059+ );
 1060+
 1061+ close.click($.fancybox.close);
 1062+ loading.click($.fancybox.cancel);
 1063+
 1064+ nav_left.click(function(e) {
 1065+ e.preventDefault();
 1066+ $.fancybox.prev();
 1067+ });
 1068+
 1069+ nav_right.click(function(e) {
 1070+ e.preventDefault();
 1071+ $.fancybox.next();
 1072+ });
 1073+
 1074+ if ($.fn.mousewheel) {
 1075+ wrap.bind('mousewheel.fb', function(e, delta) {
 1076+ if (busy) {
 1077+ e.preventDefault();
 1078+
 1079+ } else if ($(e.target).get(0).clientHeight == 0 || $(e.target).get(0).scrollHeight === $(e.target).get(0).clientHeight) {
 1080+ e.preventDefault();
 1081+ $.fancybox[ delta > 0 ? 'prev' : 'next']();
 1082+ }
 1083+ });
 1084+ }
 1085+
 1086+ if (!$.support.opacity) {
 1087+ wrap.addClass('fancybox-ie');
 1088+ }
 1089+
 1090+ if (isIE6) {
 1091+ loading.addClass('fancybox-ie6');
 1092+ wrap.addClass('fancybox-ie6');
 1093+
 1094+ $('<iframe id="fancybox-hide-sel-frame" src="' + (/^https/i.test(window.location.href || '') ? 'javascript:void(false)' : 'about:blank' ) + '" scrolling="no" border="0" frameborder="0" tabindex="-1"></iframe>').prependTo(outer);
 1095+ }
 1096+ };
 1097+
 1098+ $.fn.fancybox.defaults = {
 1099+ padding : 10,
 1100+ margin : 40,
 1101+ opacity : false,
 1102+ modal : false,
 1103+ cyclic : false,
 1104+ scrolling : 'auto', // 'auto', 'yes' or 'no'
 1105+
 1106+ width : 560,
 1107+ height : 340,
 1108+
 1109+ autoScale : true,
 1110+ autoDimensions : true,
 1111+ centerOnScroll : false,
 1112+
 1113+ ajax : {},
 1114+ swf : { wmode: 'transparent' },
 1115+
 1116+ hideOnOverlayClick : true,
 1117+ hideOnContentClick : false,
 1118+
 1119+ overlayShow : true,
 1120+ overlayOpacity : 0.7,
 1121+ overlayColor : '#777',
 1122+
 1123+ titleShow : true,
 1124+ titlePosition : 'float', // 'float', 'outside', 'inside' or 'over'
 1125+ titleFormat : null,
 1126+ titleFromAlt : false,
 1127+
 1128+ transitionIn : 'fade', // 'elastic', 'fade' or 'none'
 1129+ transitionOut : 'fade', // 'elastic', 'fade' or 'none'
 1130+
 1131+ speedIn : 300,
 1132+ speedOut : 300,
 1133+
 1134+ changeSpeed : 300,
 1135+ changeFade : 'fast',
 1136+
 1137+ easingIn : 'swing',
 1138+ easingOut : 'swing',
 1139+
 1140+ showCloseButton : true,
 1141+ showNavArrows : true,
 1142+ enableEscapeButton : true,
 1143+ enableKeyboardNav : true,
 1144+
 1145+ onStart : function(){},
 1146+ onCancel : function(){},
 1147+ onComplete : function(){},
 1148+ onCleanup : function(){},
 1149+ onClosed : function(){},
 1150+ onError : function(){}
 1151+ };
 1152+
 1153+ $(document).ready(function() {
 1154+ $.fancybox.init();
 1155+ });
 1156+
 1157+})(jQuery);
\ No newline at end of file
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/jquery.fancybox-1.3.4.js
___________________________________________________________________
Added: svn:eol-style
11158 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/fancy_shadow_n.png
Cannot display: file marked as a binary type.
svn:mime-type = image/png
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/fancy_shadow_n.png
___________________________________________________________________
Added: svn:mime-type
21159 + image/png
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/fancy_close.png
Cannot display: file marked as a binary type.
svn:mime-type = image/png
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/fancy_close.png
___________________________________________________________________
Added: svn:mime-type
31160 + image/png
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/fancy_shadow_ne.png
Cannot display: file marked as a binary type.
svn:mime-type = image/png
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/fancy_shadow_ne.png
___________________________________________________________________
Added: svn:mime-type
41161 + image/png
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/fancy_shadow_s.png
Cannot display: file marked as a binary type.
svn:mime-type = image/png
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/fancy_shadow_s.png
___________________________________________________________________
Added: svn:mime-type
51162 + image/png
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/jquery.easing-1.3.pack.js
@@ -0,0 +1,72 @@
 2+/*
 3+ * jQuery Easing v1.3 - http://gsgd.co.uk/sandbox/jquery/easing/
 4+ *
 5+ * Uses the built in easing capabilities added In jQuery 1.1
 6+ * to offer multiple easing options
 7+ *
 8+ * TERMS OF USE - jQuery Easing
 9+ *
 10+ * Open source under the BSD License.
 11+ *
 12+ * Copyright © 2008 George McGinley Smith
 13+ * All rights reserved.
 14+ *
 15+ * Redistribution and use in source and binary forms, with or without modification,
 16+ * are permitted provided that the following conditions are met:
 17+ *
 18+ * Redistributions of source code must retain the above copyright notice, this list of
 19+ * conditions and the following disclaimer.
 20+ * Redistributions in binary form must reproduce the above copyright notice, this list
 21+ * of conditions and the following disclaimer in the documentation and/or other materials
 22+ * provided with the distribution.
 23+ *
 24+ * Neither the name of the author nor the names of contributors may be used to endorse
 25+ * or promote products derived from this software without specific prior written permission.
 26+ *
 27+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
 28+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 29+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 30+ * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 31+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
 32+ * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
 33+ * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 34+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 35+ * OF THE POSSIBILITY OF SUCH DAMAGE.
 36+ *
 37+*/
 38+
 39+// t: current time, b: begInnIng value, c: change In value, d: duration
 40+eval(function(p,a,c,k,e,r){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('h.i[\'1a\']=h.i[\'z\'];h.O(h.i,{y:\'D\',z:9(x,t,b,c,d){6 h.i[h.i.y](x,t,b,c,d)},17:9(x,t,b,c,d){6 c*(t/=d)*t+b},D:9(x,t,b,c,d){6-c*(t/=d)*(t-2)+b},13:9(x,t,b,c,d){e((t/=d/2)<1)6 c/2*t*t+b;6-c/2*((--t)*(t-2)-1)+b},X:9(x,t,b,c,d){6 c*(t/=d)*t*t+b},U:9(x,t,b,c,d){6 c*((t=t/d-1)*t*t+1)+b},R:9(x,t,b,c,d){e((t/=d/2)<1)6 c/2*t*t*t+b;6 c/2*((t-=2)*t*t+2)+b},N:9(x,t,b,c,d){6 c*(t/=d)*t*t*t+b},M:9(x,t,b,c,d){6-c*((t=t/d-1)*t*t*t-1)+b},L:9(x,t,b,c,d){e((t/=d/2)<1)6 c/2*t*t*t*t+b;6-c/2*((t-=2)*t*t*t-2)+b},K:9(x,t,b,c,d){6 c*(t/=d)*t*t*t*t+b},J:9(x,t,b,c,d){6 c*((t=t/d-1)*t*t*t*t+1)+b},I:9(x,t,b,c,d){e((t/=d/2)<1)6 c/2*t*t*t*t*t+b;6 c/2*((t-=2)*t*t*t*t+2)+b},G:9(x,t,b,c,d){6-c*8.C(t/d*(8.g/2))+c+b},15:9(x,t,b,c,d){6 c*8.n(t/d*(8.g/2))+b},12:9(x,t,b,c,d){6-c/2*(8.C(8.g*t/d)-1)+b},Z:9(x,t,b,c,d){6(t==0)?b:c*8.j(2,10*(t/d-1))+b},Y:9(x,t,b,c,d){6(t==d)?b+c:c*(-8.j(2,-10*t/d)+1)+b},W:9(x,t,b,c,d){e(t==0)6 b;e(t==d)6 b+c;e((t/=d/2)<1)6 c/2*8.j(2,10*(t-1))+b;6 c/2*(-8.j(2,-10*--t)+2)+b},V:9(x,t,b,c,d){6-c*(8.o(1-(t/=d)*t)-1)+b},S:9(x,t,b,c,d){6 c*8.o(1-(t=t/d-1)*t)+b},Q:9(x,t,b,c,d){e((t/=d/2)<1)6-c/2*(8.o(1-t*t)-1)+b;6 c/2*(8.o(1-(t-=2)*t)+1)+b},P:9(x,t,b,c,d){f s=1.l;f p=0;f a=c;e(t==0)6 b;e((t/=d)==1)6 b+c;e(!p)p=d*.3;e(a<8.w(c)){a=c;f s=p/4}m f s=p/(2*8.g)*8.r(c/a);6-(a*8.j(2,10*(t-=1))*8.n((t*d-s)*(2*8.g)/p))+b},H:9(x,t,b,c,d){f s=1.l;f p=0;f a=c;e(t==0)6 b;e((t/=d)==1)6 b+c;e(!p)p=d*.3;e(a<8.w(c)){a=c;f s=p/4}m f s=p/(2*8.g)*8.r(c/a);6 a*8.j(2,-10*t)*8.n((t*d-s)*(2*8.g)/p)+c+b},T:9(x,t,b,c,d){f s=1.l;f p=0;f a=c;e(t==0)6 b;e((t/=d/2)==2)6 b+c;e(!p)p=d*(.3*1.5);e(a<8.w(c)){a=c;f s=p/4}m f s=p/(2*8.g)*8.r(c/a);e(t<1)6-.5*(a*8.j(2,10*(t-=1))*8.n((t*d-s)*(2*8.g)/p))+b;6 a*8.j(2,-10*(t-=1))*8.n((t*d-s)*(2*8.g)/p)*.5+c+b},F:9(x,t,b,c,d,s){e(s==u)s=1.l;6 c*(t/=d)*t*((s+1)*t-s)+b},E:9(x,t,b,c,d,s){e(s==u)s=1.l;6 c*((t=t/d-1)*t*((s+1)*t+s)+1)+b},16:9(x,t,b,c,d,s){e(s==u)s=1.l;e((t/=d/2)<1)6 c/2*(t*t*(((s*=(1.B))+1)*t-s))+b;6 c/2*((t-=2)*t*(((s*=(1.B))+1)*t+s)+2)+b},A:9(x,t,b,c,d){6 c-h.i.v(x,d-t,0,c,d)+b},v:9(x,t,b,c,d){e((t/=d)<(1/2.k)){6 c*(7.q*t*t)+b}m e(t<(2/2.k)){6 c*(7.q*(t-=(1.5/2.k))*t+.k)+b}m e(t<(2.5/2.k)){6 c*(7.q*(t-=(2.14/2.k))*t+.11)+b}m{6 c*(7.q*(t-=(2.18/2.k))*t+.19)+b}},1b:9(x,t,b,c,d){e(t<d/2)6 h.i.A(x,t*2,0,c,d)*.5+b;6 h.i.v(x,t*2-d,0,c,d)*.5+c*.5+b}});',62,74,'||||||return||Math|function|||||if|var|PI|jQuery|easing|pow|75|70158|else|sin|sqrt||5625|asin|||undefined|easeOutBounce|abs||def|swing|easeInBounce|525|cos|easeOutQuad|easeOutBack|easeInBack|easeInSine|easeOutElastic|easeInOutQuint|easeOutQuint|easeInQuint|easeInOutQuart|easeOutQuart|easeInQuart|extend|easeInElastic|easeInOutCirc|easeInOutCubic|easeOutCirc|easeInOutElastic|easeOutCubic|easeInCirc|easeInOutExpo|easeInCubic|easeOutExpo|easeInExpo||9375|easeInOutSine|easeInOutQuad|25|easeOutSine|easeInOutBack|easeInQuad|625|984375|jswing|easeInOutBounce'.split('|'),0,{}))
 41+
 42+/*
 43+ *
 44+ * TERMS OF USE - EASING EQUATIONS
 45+ *
 46+ * Open source under the BSD License.
 47+ *
 48+ * Copyright © 2001 Robert Penner
 49+ * All rights reserved.
 50+ *
 51+ * Redistribution and use in source and binary forms, with or without modification,
 52+ * are permitted provided that the following conditions are met:
 53+ *
 54+ * Redistributions of source code must retain the above copyright notice, this list of
 55+ * conditions and the following disclaimer.
 56+ * Redistributions in binary form must reproduce the above copyright notice, this list
 57+ * of conditions and the following disclaimer in the documentation and/or other materials
 58+ * provided with the distribution.
 59+ *
 60+ * Neither the name of the author nor the names of contributors may be used to endorse
 61+ * or promote products derived from this software without specific prior written permission.
 62+ *
 63+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
 64+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 65+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 66+ * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 67+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
 68+ * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
 69+ * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 70+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 71+ * OF THE POSSIBILITY OF SUCH DAMAGE.
 72+ *
 73+ */
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/jquery.easing-1.3.pack.js
___________________________________________________________________
Added: svn:eol-style
174 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/fancy_shadow_w.png
Cannot display: file marked as a binary type.
svn:mime-type = image/png
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/fancy_shadow_w.png
___________________________________________________________________
Added: svn:mime-type
275 + image/png
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/jquery.fancybox-1.3.4.css
@@ -0,0 +1,359 @@
 2+/*
 3+ * FancyBox - jQuery Plugin
 4+ * Simple and fancy lightbox alternative
 5+ *
 6+ * Examples and documentation at: http://fancybox.net
 7+ *
 8+ * Copyright (c) 2008 - 2010 Janis Skarnelis
 9+ * That said, it is hardly a one-person project. Many people have submitted bugs, code, and offered their advice freely. Their support is greatly appreciated.
 10+ *
 11+ * Version: 1.3.4 (11/11/2010)
 12+ * Requires: jQuery v1.3+
 13+ *
 14+ * Dual licensed under the MIT and GPL licenses:
 15+ * http://www.opensource.org/licenses/mit-license.php
 16+ * http://www.gnu.org/licenses/gpl.html
 17+ */
 18+
 19+#fancybox-loading {
 20+ position: fixed;
 21+ top: 50%;
 22+ left: 50%;
 23+ width: 40px;
 24+ height: 40px;
 25+ margin-top: -20px;
 26+ margin-left: -20px;
 27+ cursor: pointer;
 28+ overflow: hidden;
 29+ z-index: 1104;
 30+ display: none;
 31+}
 32+
 33+#fancybox-loading div {
 34+ position: absolute;
 35+ top: 0;
 36+ left: 0;
 37+ width: 40px;
 38+ height: 480px;
 39+ background-image: url('fancybox.png');
 40+}
 41+
 42+#fancybox-overlay {
 43+ position: absolute;
 44+ top: 0;
 45+ left: 0;
 46+ width: 100%;
 47+ z-index: 1100;
 48+ display: none;
 49+}
 50+
 51+#fancybox-tmp {
 52+ padding: 0;
 53+ margin: 0;
 54+ border: 0;
 55+ overflow: auto;
 56+ display: none;
 57+}
 58+
 59+#fancybox-wrap {
 60+ position: absolute;
 61+ top: 0;
 62+ left: 0;
 63+ padding: 20px;
 64+ z-index: 1101;
 65+ outline: none;
 66+ display: none;
 67+}
 68+
 69+#fancybox-outer {
 70+ position: relative;
 71+ width: 100%;
 72+ height: 100%;
 73+ background: #fff;
 74+}
 75+
 76+#fancybox-content {
 77+ width: 0;
 78+ height: 0;
 79+ padding: 0;
 80+ outline: none;
 81+ position: relative;
 82+ overflow: hidden;
 83+ z-index: 1102;
 84+ border: 0px solid #fff;
 85+}
 86+
 87+#fancybox-hide-sel-frame {
 88+ position: absolute;
 89+ top: 0;
 90+ left: 0;
 91+ width: 100%;
 92+ height: 100%;
 93+ background: transparent;
 94+ z-index: 1101;
 95+}
 96+
 97+#fancybox-close {
 98+ position: absolute;
 99+ top: -15px;
 100+ right: -15px;
 101+ width: 30px;
 102+ height: 30px;
 103+ background: transparent url('fancybox.png') -40px 0px;
 104+ cursor: pointer;
 105+ z-index: 1103;
 106+ display: none;
 107+}
 108+
 109+#fancybox-error {
 110+ color: #444;
 111+ font: normal 12px/20px Arial;
 112+ padding: 14px;
 113+ margin: 0;
 114+}
 115+
 116+#fancybox-img {
 117+ width: 100%;
 118+ height: 100%;
 119+ padding: 0;
 120+ margin: 0;
 121+ border: none;
 122+ outline: none;
 123+ line-height: 0;
 124+ vertical-align: top;
 125+}
 126+
 127+#fancybox-frame {
 128+ width: 100%;
 129+ height: 100%;
 130+ border: none;
 131+ display: block;
 132+}
 133+
 134+#fancybox-left, #fancybox-right {
 135+ position: absolute;
 136+ bottom: 0px;
 137+ height: 100%;
 138+ width: 35%;
 139+ cursor: pointer;
 140+ outline: none;
 141+ background: transparent url('blank.gif');
 142+ z-index: 1102;
 143+ display: none;
 144+}
 145+
 146+#fancybox-left {
 147+ left: 0px;
 148+}
 149+
 150+#fancybox-right {
 151+ right: 0px;
 152+}
 153+
 154+#fancybox-left-ico, #fancybox-right-ico {
 155+ position: absolute;
 156+ top: 50%;
 157+ left: -9999px;
 158+ width: 30px;
 159+ height: 30px;
 160+ margin-top: -15px;
 161+ cursor: pointer;
 162+ z-index: 1102;
 163+ display: block;
 164+}
 165+
 166+#fancybox-left-ico {
 167+ background-image: url('fancybox.png');
 168+ background-position: -40px -30px;
 169+}
 170+
 171+#fancybox-right-ico {
 172+ background-image: url('fancybox.png');
 173+ background-position: -40px -60px;
 174+}
 175+
 176+#fancybox-left:hover, #fancybox-right:hover {
 177+ visibility: visible; /* IE6 */
 178+}
 179+
 180+#fancybox-left:hover span {
 181+ left: 20px;
 182+}
 183+
 184+#fancybox-right:hover span {
 185+ left: auto;
 186+ right: 20px;
 187+}
 188+
 189+.fancybox-bg {
 190+ position: absolute;
 191+ padding: 0;
 192+ margin: 0;
 193+ border: 0;
 194+ width: 20px;
 195+ height: 20px;
 196+ z-index: 1001;
 197+}
 198+
 199+#fancybox-bg-n {
 200+ top: -20px;
 201+ left: 0;
 202+ width: 100%;
 203+ background-image: url('fancybox-x.png');
 204+}
 205+
 206+#fancybox-bg-ne {
 207+ top: -20px;
 208+ right: -20px;
 209+ background-image: url('fancybox.png');
 210+ background-position: -40px -162px;
 211+}
 212+
 213+#fancybox-bg-e {
 214+ top: 0;
 215+ right: -20px;
 216+ height: 100%;
 217+ background-image: url('fancybox-y.png');
 218+ background-position: -20px 0px;
 219+}
 220+
 221+#fancybox-bg-se {
 222+ bottom: -20px;
 223+ right: -20px;
 224+ background-image: url('fancybox.png');
 225+ background-position: -40px -182px;
 226+}
 227+
 228+#fancybox-bg-s {
 229+ bottom: -20px;
 230+ left: 0;
 231+ width: 100%;
 232+ background-image: url('fancybox-x.png');
 233+ background-position: 0px -20px;
 234+}
 235+
 236+#fancybox-bg-sw {
 237+ bottom: -20px;
 238+ left: -20px;
 239+ background-image: url('fancybox.png');
 240+ background-position: -40px -142px;
 241+}
 242+
 243+#fancybox-bg-w {
 244+ top: 0;
 245+ left: -20px;
 246+ height: 100%;
 247+ background-image: url('fancybox-y.png');
 248+}
 249+
 250+#fancybox-bg-nw {
 251+ top: -20px;
 252+ left: -20px;
 253+ background-image: url('fancybox.png');
 254+ background-position: -40px -122px;
 255+}
 256+
 257+#fancybox-title {
 258+ font-family: Helvetica;
 259+ font-size: 12px;
 260+ z-index: 1102;
 261+}
 262+
 263+.fancybox-title-inside {
 264+ padding-bottom: 10px;
 265+ text-align: center;
 266+ color: #333;
 267+ background: #fff;
 268+ position: relative;
 269+}
 270+
 271+.fancybox-title-outside {
 272+ padding-top: 10px;
 273+ color: #fff;
 274+}
 275+
 276+.fancybox-title-over {
 277+ position: absolute;
 278+ bottom: 0;
 279+ left: 0;
 280+ color: #FFF;
 281+ text-align: left;
 282+}
 283+
 284+#fancybox-title-over {
 285+ padding: 10px;
 286+ background-image: url('fancy_title_over.png');
 287+ display: block;
 288+}
 289+
 290+.fancybox-title-float {
 291+ position: absolute;
 292+ left: 0;
 293+ bottom: -20px;
 294+ height: 32px;
 295+}
 296+
 297+#fancybox-title-float-wrap {
 298+ border: none;
 299+ border-collapse: collapse;
 300+ width: auto;
 301+}
 302+
 303+#fancybox-title-float-wrap td {
 304+ border: none;
 305+ white-space: nowrap;
 306+}
 307+
 308+#fancybox-title-float-left {
 309+ padding: 0 0 0 15px;
 310+ background: url('fancybox.png') -40px -90px no-repeat;
 311+}
 312+
 313+#fancybox-title-float-main {
 314+ color: #FFF;
 315+ line-height: 29px;
 316+ font-weight: bold;
 317+ padding: 0 0 3px 0;
 318+ background: url('fancybox-x.png') 0px -40px;
 319+}
 320+
 321+#fancybox-title-float-right {
 322+ padding: 0 0 0 15px;
 323+ background: url('fancybox.png') -55px -90px no-repeat;
 324+}
 325+
 326+/* IE6 */
 327+
 328+.fancybox-ie6 #fancybox-close { background: transparent; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_close.png', sizingMethod='scale'); }
 329+
 330+.fancybox-ie6 #fancybox-left-ico { background: transparent; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_nav_left.png', sizingMethod='scale'); }
 331+.fancybox-ie6 #fancybox-right-ico { background: transparent; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_nav_right.png', sizingMethod='scale'); }
 332+
 333+.fancybox-ie6 #fancybox-title-over { background: transparent; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_title_over.png', sizingMethod='scale'); zoom: 1; }
 334+.fancybox-ie6 #fancybox-title-float-left { background: transparent; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_title_left.png', sizingMethod='scale'); }
 335+.fancybox-ie6 #fancybox-title-float-main { background: transparent; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_title_main.png', sizingMethod='scale'); }
 336+.fancybox-ie6 #fancybox-title-float-right { background: transparent; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_title_right.png', sizingMethod='scale'); }
 337+
 338+.fancybox-ie6 #fancybox-bg-w, .fancybox-ie6 #fancybox-bg-e, .fancybox-ie6 #fancybox-left, .fancybox-ie6 #fancybox-right, #fancybox-hide-sel-frame {
 339+ height: expression(this.parentNode.clientHeight + "px");
 340+}
 341+
 342+#fancybox-loading.fancybox-ie6 {
 343+ position: absolute; margin-top: 0;
 344+ top: expression( (-20 + (document.documentElement.clientHeight ? document.documentElement.clientHeight/2 : document.body.clientHeight/2 ) + ( ignoreMe = document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop )) + 'px');
 345+}
 346+
 347+#fancybox-loading.fancybox-ie6 div { background: transparent; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_loading.png', sizingMethod='scale'); }
 348+
 349+/* IE6, IE7, IE8 */
 350+
 351+.fancybox-ie .fancybox-bg { background: transparent !important; }
 352+
 353+.fancybox-ie #fancybox-bg-n { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_shadow_n.png', sizingMethod='scale'); }
 354+.fancybox-ie #fancybox-bg-ne { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_shadow_ne.png', sizingMethod='scale'); }
 355+.fancybox-ie #fancybox-bg-e { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_shadow_e.png', sizingMethod='scale'); }
 356+.fancybox-ie #fancybox-bg-se { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_shadow_se.png', sizingMethod='scale'); }
 357+.fancybox-ie #fancybox-bg-s { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_shadow_s.png', sizingMethod='scale'); }
 358+.fancybox-ie #fancybox-bg-sw { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_shadow_sw.png', sizingMethod='scale'); }
 359+.fancybox-ie #fancybox-bg-w { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_shadow_w.png', sizingMethod='scale'); }
 360+.fancybox-ie #fancybox-bg-nw { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='fancybox/fancy_shadow_nw.png', sizingMethod='scale'); }
\ No newline at end of file
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/jquery.fancybox-1.3.4.css
___________________________________________________________________
Added: svn:eol-style
1361 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/fancy_shadow_se.png
Cannot display: file marked as a binary type.
svn:mime-type = image/png
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/fancy_shadow_se.png
___________________________________________________________________
Added: svn:mime-type
2362 + image/png
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/fancy_title_over.png
Cannot display: file marked as a binary type.
svn:mime-type = image/png
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/fancy_title_over.png
___________________________________________________________________
Added: svn:mime-type
3363 + image/png
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/jquery.fancybox-1.3.4.pack.js
@@ -0,0 +1,46 @@
 2+/*
 3+ * FancyBox - jQuery Plugin
 4+ * Simple and fancy lightbox alternative
 5+ *
 6+ * Examples and documentation at: http://fancybox.net
 7+ *
 8+ * Copyright (c) 2008 - 2010 Janis Skarnelis
 9+ * That said, it is hardly a one-person project. Many people have submitted bugs, code, and offered their advice freely. Their support is greatly appreciated.
 10+ *
 11+ * Version: 1.3.4 (11/11/2010)
 12+ * Requires: jQuery v1.3+
 13+ *
 14+ * Dual licensed under the MIT and GPL licenses:
 15+ * http://www.opensource.org/licenses/mit-license.php
 16+ * http://www.gnu.org/licenses/gpl.html
 17+ */
 18+
 19+;(function(b){var m,t,u,f,D,j,E,n,z,A,q=0,e={},o=[],p=0,d={},l=[],G=null,v=new Image,J=/\.(jpg|gif|png|bmp|jpeg)(.*)?$/i,W=/[^\.]\.(swf)\s*$/i,K,L=1,y=0,s="",r,i,h=false,B=b.extend(b("<div/>")[0],{prop:0}),M=b.browser.msie&&b.browser.version<7&&!window.XMLHttpRequest,N=function(){t.hide();v.onerror=v.onload=null;G&&G.abort();m.empty()},O=function(){if(false===e.onError(o,q,e)){t.hide();h=false}else{e.titleShow=false;e.width="auto";e.height="auto";m.html('<p id="fancybox-error">The requested content cannot be loaded.<br />Please try again later.</p>');
 20+F()}},I=function(){var a=o[q],c,g,k,C,P,w;N();e=b.extend({},b.fn.fancybox.defaults,typeof b(a).data("fancybox")=="undefined"?e:b(a).data("fancybox"));w=e.onStart(o,q,e);if(w===false)h=false;else{if(typeof w=="object")e=b.extend(e,w);k=e.title||(a.nodeName?b(a).attr("title"):a.title)||"";if(a.nodeName&&!e.orig)e.orig=b(a).children("img:first").length?b(a).children("img:first"):b(a);if(k===""&&e.orig&&e.titleFromAlt)k=e.orig.attr("alt");c=e.href||(a.nodeName?b(a).attr("href"):a.href)||null;if(/^(?:javascript)/i.test(c)||
 21+c=="#")c=null;if(e.type){g=e.type;if(!c)c=e.content}else if(e.content)g="html";else if(c)g=c.match(J)?"image":c.match(W)?"swf":b(a).hasClass("iframe")?"iframe":c.indexOf("#")===0?"inline":"ajax";if(g){if(g=="inline"){a=c.substr(c.indexOf("#"));g=b(a).length>0?"inline":"ajax"}e.type=g;e.href=c;e.title=k;if(e.autoDimensions)if(e.type=="html"||e.type=="inline"||e.type=="ajax"){e.width="auto";e.height="auto"}else e.autoDimensions=false;if(e.modal){e.overlayShow=true;e.hideOnOverlayClick=false;e.hideOnContentClick=
 22+false;e.enableEscapeButton=false;e.showCloseButton=false}e.padding=parseInt(e.padding,10);e.margin=parseInt(e.margin,10);m.css("padding",e.padding+e.margin);b(".fancybox-inline-tmp").unbind("fancybox-cancel").bind("fancybox-change",function(){b(this).replaceWith(j.children())});switch(g){case "html":m.html(e.content);F();break;case "inline":if(b(a).parent().is("#fancybox-content")===true){h=false;break}b('<div class="fancybox-inline-tmp" />').hide().insertBefore(b(a)).bind("fancybox-cleanup",function(){b(this).replaceWith(j.children())}).bind("fancybox-cancel",
 23+function(){b(this).replaceWith(m.children())});b(a).appendTo(m);F();break;case "image":h=false;b.fancybox.showActivity();v=new Image;v.onerror=function(){O()};v.onload=function(){h=true;v.onerror=v.onload=null;e.width=v.width;e.height=v.height;b("<img />").attr({id:"fancybox-img",src:v.src,alt:e.title}).appendTo(m);Q()};v.src=c;break;case "swf":e.scrolling="no";C='<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" width="'+e.width+'" height="'+e.height+'"><param name="movie" value="'+c+
 24+'"></param>';P="";b.each(e.swf,function(x,H){C+='<param name="'+x+'" value="'+H+'"></param>';P+=" "+x+'="'+H+'"'});C+='<embed src="'+c+'" type="application/x-shockwave-flash" width="'+e.width+'" height="'+e.height+'"'+P+"></embed></object>";m.html(C);F();break;case "ajax":h=false;b.fancybox.showActivity();e.ajax.win=e.ajax.success;G=b.ajax(b.extend({},e.ajax,{url:c,data:e.ajax.data||{},error:function(x){x.status>0&&O()},success:function(x,H,R){if((typeof R=="object"?R:G).status==200){if(typeof e.ajax.win==
 25+"function"){w=e.ajax.win(c,x,H,R);if(w===false){t.hide();return}else if(typeof w=="string"||typeof w=="object")x=w}m.html(x);F()}}}));break;case "iframe":Q()}}else O()}},F=function(){var a=e.width,c=e.height;a=a.toString().indexOf("%")>-1?parseInt((b(window).width()-e.margin*2)*parseFloat(a)/100,10)+"px":a=="auto"?"auto":a+"px";c=c.toString().indexOf("%")>-1?parseInt((b(window).height()-e.margin*2)*parseFloat(c)/100,10)+"px":c=="auto"?"auto":c+"px";m.wrapInner('<div style="width:'+a+";height:"+c+
 26+";overflow: "+(e.scrolling=="auto"?"auto":e.scrolling=="yes"?"scroll":"hidden")+';position:relative;"></div>');e.width=m.width();e.height=m.height();Q()},Q=function(){var a,c;t.hide();if(f.is(":visible")&&false===d.onCleanup(l,p,d)){b.event.trigger("fancybox-cancel");h=false}else{h=true;b(j.add(u)).unbind();b(window).unbind("resize.fb scroll.fb");b(document).unbind("keydown.fb");f.is(":visible")&&d.titlePosition!=="outside"&&f.css("height",f.height());l=o;p=q;d=e;if(d.overlayShow){u.css({"background-color":d.overlayColor,
 27+opacity:d.overlayOpacity,cursor:d.hideOnOverlayClick?"pointer":"auto",height:b(document).height()});if(!u.is(":visible")){M&&b("select:not(#fancybox-tmp select)").filter(function(){return this.style.visibility!=="hidden"}).css({visibility:"hidden"}).one("fancybox-cleanup",function(){this.style.visibility="inherit"});u.show()}}else u.hide();i=X();s=d.title||"";y=0;n.empty().removeAttr("style").removeClass();if(d.titleShow!==false){if(b.isFunction(d.titleFormat))a=d.titleFormat(s,l,p,d);else a=s&&s.length?
 28+d.titlePosition=="float"?'<table id="fancybox-title-float-wrap" cellpadding="0" cellspacing="0"><tr><td id="fancybox-title-float-left"></td><td id="fancybox-title-float-main">'+s+'</td><td id="fancybox-title-float-right"></td></tr></table>':'<div id="fancybox-title-'+d.titlePosition+'">'+s+"</div>":false;s=a;if(!(!s||s==="")){n.addClass("fancybox-title-"+d.titlePosition).html(s).appendTo("body").show();switch(d.titlePosition){case "inside":n.css({width:i.width-d.padding*2,marginLeft:d.padding,marginRight:d.padding});
 29+y=n.outerHeight(true);n.appendTo(D);i.height+=y;break;case "over":n.css({marginLeft:d.padding,width:i.width-d.padding*2,bottom:d.padding}).appendTo(D);break;case "float":n.css("left",parseInt((n.width()-i.width-40)/2,10)*-1).appendTo(f);break;default:n.css({width:i.width-d.padding*2,paddingLeft:d.padding,paddingRight:d.padding}).appendTo(f)}}}n.hide();if(f.is(":visible")){b(E.add(z).add(A)).hide();a=f.position();r={top:a.top,left:a.left,width:f.width(),height:f.height()};c=r.width==i.width&&r.height==
 30+i.height;j.fadeTo(d.changeFade,0.3,function(){var g=function(){j.html(m.contents()).fadeTo(d.changeFade,1,S)};b.event.trigger("fancybox-change");j.empty().removeAttr("filter").css({"border-width":d.padding,width:i.width-d.padding*2,height:e.autoDimensions?"auto":i.height-y-d.padding*2});if(c)g();else{B.prop=0;b(B).animate({prop:1},{duration:d.changeSpeed,easing:d.easingChange,step:T,complete:g})}})}else{f.removeAttr("style");j.css("border-width",d.padding);if(d.transitionIn=="elastic"){r=V();j.html(m.contents());
 31+f.show();if(d.opacity)i.opacity=0;B.prop=0;b(B).animate({prop:1},{duration:d.speedIn,easing:d.easingIn,step:T,complete:S})}else{d.titlePosition=="inside"&&y>0&&n.show();j.css({width:i.width-d.padding*2,height:e.autoDimensions?"auto":i.height-y-d.padding*2}).html(m.contents());f.css(i).fadeIn(d.transitionIn=="none"?0:d.speedIn,S)}}}},Y=function(){if(d.enableEscapeButton||d.enableKeyboardNav)b(document).bind("keydown.fb",function(a){if(a.keyCode==27&&d.enableEscapeButton){a.preventDefault();b.fancybox.close()}else if((a.keyCode==
 32+37||a.keyCode==39)&&d.enableKeyboardNav&&a.target.tagName!=="INPUT"&&a.target.tagName!=="TEXTAREA"&&a.target.tagName!=="SELECT"){a.preventDefault();b.fancybox[a.keyCode==37?"prev":"next"]()}});if(d.showNavArrows){if(d.cyclic&&l.length>1||p!==0)z.show();if(d.cyclic&&l.length>1||p!=l.length-1)A.show()}else{z.hide();A.hide()}},S=function(){if(!b.support.opacity){j.get(0).style.removeAttribute("filter");f.get(0).style.removeAttribute("filter")}e.autoDimensions&&j.css("height","auto");f.css("height","auto");
 33+s&&s.length&&n.show();d.showCloseButton&&E.show();Y();d.hideOnContentClick&&j.bind("click",b.fancybox.close);d.hideOnOverlayClick&&u.bind("click",b.fancybox.close);b(window).bind("resize.fb",b.fancybox.resize);d.centerOnScroll&&b(window).bind("scroll.fb",b.fancybox.center);if(d.type=="iframe")b('<iframe id="fancybox-frame" name="fancybox-frame'+(new Date).getTime()+'" frameborder="0" hspace="0" '+(b.browser.msie?'allowtransparency="true""':"")+' scrolling="'+e.scrolling+'" src="'+d.href+'"></iframe>').appendTo(j);
 34+f.show();h=false;b.fancybox.center();d.onComplete(l,p,d);var a,c;if(l.length-1>p){a=l[p+1].href;if(typeof a!=="undefined"&&a.match(J)){c=new Image;c.src=a}}if(p>0){a=l[p-1].href;if(typeof a!=="undefined"&&a.match(J)){c=new Image;c.src=a}}},T=function(a){var c={width:parseInt(r.width+(i.width-r.width)*a,10),height:parseInt(r.height+(i.height-r.height)*a,10),top:parseInt(r.top+(i.top-r.top)*a,10),left:parseInt(r.left+(i.left-r.left)*a,10)};if(typeof i.opacity!=="undefined")c.opacity=a<0.5?0.5:a;f.css(c);
 35+j.css({width:c.width-d.padding*2,height:c.height-y*a-d.padding*2})},U=function(){return[b(window).width()-d.margin*2,b(window).height()-d.margin*2,b(document).scrollLeft()+d.margin,b(document).scrollTop()+d.margin]},X=function(){var a=U(),c={},g=d.autoScale,k=d.padding*2;c.width=d.width.toString().indexOf("%")>-1?parseInt(a[0]*parseFloat(d.width)/100,10):d.width+k;c.height=d.height.toString().indexOf("%")>-1?parseInt(a[1]*parseFloat(d.height)/100,10):d.height+k;if(g&&(c.width>a[0]||c.height>a[1]))if(e.type==
 36+"image"||e.type=="swf"){g=d.width/d.height;if(c.width>a[0]){c.width=a[0];c.height=parseInt((c.width-k)/g+k,10)}if(c.height>a[1]){c.height=a[1];c.width=parseInt((c.height-k)*g+k,10)}}else{c.width=Math.min(c.width,a[0]);c.height=Math.min(c.height,a[1])}c.top=parseInt(Math.max(a[3]-20,a[3]+(a[1]-c.height-40)*0.5),10);c.left=parseInt(Math.max(a[2]-20,a[2]+(a[0]-c.width-40)*0.5),10);return c},V=function(){var a=e.orig?b(e.orig):false,c={};if(a&&a.length){c=a.offset();c.top+=parseInt(a.css("paddingTop"),
 37+10)||0;c.left+=parseInt(a.css("paddingLeft"),10)||0;c.top+=parseInt(a.css("border-top-width"),10)||0;c.left+=parseInt(a.css("border-left-width"),10)||0;c.width=a.width();c.height=a.height();c={width:c.width+d.padding*2,height:c.height+d.padding*2,top:c.top-d.padding-20,left:c.left-d.padding-20}}else{a=U();c={width:d.padding*2,height:d.padding*2,top:parseInt(a[3]+a[1]*0.5,10),left:parseInt(a[2]+a[0]*0.5,10)}}return c},Z=function(){if(t.is(":visible")){b("div",t).css("top",L*-40+"px");L=(L+1)%12}else clearInterval(K)};
 38+b.fn.fancybox=function(a){if(!b(this).length)return this;b(this).data("fancybox",b.extend({},a,b.metadata?b(this).metadata():{})).unbind("click.fb").bind("click.fb",function(c){c.preventDefault();if(!h){h=true;b(this).blur();o=[];q=0;c=b(this).attr("rel")||"";if(!c||c==""||c==="nofollow")o.push(this);else{o=b("a[rel="+c+"], area[rel="+c+"]");q=o.index(this)}I()}});return this};b.fancybox=function(a,c){var g;if(!h){h=true;g=typeof c!=="undefined"?c:{};o=[];q=parseInt(g.index,10)||0;if(b.isArray(a)){for(var k=
 39+0,C=a.length;k<C;k++)if(typeof a[k]=="object")b(a[k]).data("fancybox",b.extend({},g,a[k]));else a[k]=b({}).data("fancybox",b.extend({content:a[k]},g));o=jQuery.merge(o,a)}else{if(typeof a=="object")b(a).data("fancybox",b.extend({},g,a));else a=b({}).data("fancybox",b.extend({content:a},g));o.push(a)}if(q>o.length||q<0)q=0;I()}};b.fancybox.showActivity=function(){clearInterval(K);t.show();K=setInterval(Z,66)};b.fancybox.hideActivity=function(){t.hide()};b.fancybox.next=function(){return b.fancybox.pos(p+
 40+1)};b.fancybox.prev=function(){return b.fancybox.pos(p-1)};b.fancybox.pos=function(a){if(!h){a=parseInt(a);o=l;if(a>-1&&a<l.length){q=a;I()}else if(d.cyclic&&l.length>1){q=a>=l.length?0:l.length-1;I()}}};b.fancybox.cancel=function(){if(!h){h=true;b.event.trigger("fancybox-cancel");N();e.onCancel(o,q,e);h=false}};b.fancybox.close=function(){function a(){u.fadeOut("fast");n.empty().hide();f.hide();b.event.trigger("fancybox-cleanup");j.empty();d.onClosed(l,p,d);l=e=[];p=q=0;d=e={};h=false}if(!(h||f.is(":hidden"))){h=
 41+true;if(d&&false===d.onCleanup(l,p,d))h=false;else{N();b(E.add(z).add(A)).hide();b(j.add(u)).unbind();b(window).unbind("resize.fb scroll.fb");b(document).unbind("keydown.fb");j.find("iframe").attr("src",M&&/^https/i.test(window.location.href||"")?"javascript:void(false)":"about:blank");d.titlePosition!=="inside"&&n.empty();f.stop();if(d.transitionOut=="elastic"){r=V();var c=f.position();i={top:c.top,left:c.left,width:f.width(),height:f.height()};if(d.opacity)i.opacity=1;n.empty().hide();B.prop=1;
 42+b(B).animate({prop:0},{duration:d.speedOut,easing:d.easingOut,step:T,complete:a})}else f.fadeOut(d.transitionOut=="none"?0:d.speedOut,a)}}};b.fancybox.resize=function(){u.is(":visible")&&u.css("height",b(document).height());b.fancybox.center(true)};b.fancybox.center=function(a){var c,g;if(!h){g=a===true?1:0;c=U();!g&&(f.width()>c[0]||f.height()>c[1])||f.stop().animate({top:parseInt(Math.max(c[3]-20,c[3]+(c[1]-j.height()-40)*0.5-d.padding)),left:parseInt(Math.max(c[2]-20,c[2]+(c[0]-j.width()-40)*0.5-
 43+d.padding))},typeof a=="number"?a:200)}};b.fancybox.init=function(){if(!b("#fancybox-wrap").length){b("body").append(m=b('<div id="fancybox-tmp"></div>'),t=b('<div id="fancybox-loading"><div></div></div>'),u=b('<div id="fancybox-overlay"></div>'),f=b('<div id="fancybox-wrap"></div>'));D=b('<div id="fancybox-outer"></div>').append('<div class="fancybox-bg" id="fancybox-bg-n"></div><div class="fancybox-bg" id="fancybox-bg-ne"></div><div class="fancybox-bg" id="fancybox-bg-e"></div><div class="fancybox-bg" id="fancybox-bg-se"></div><div class="fancybox-bg" id="fancybox-bg-s"></div><div class="fancybox-bg" id="fancybox-bg-sw"></div><div class="fancybox-bg" id="fancybox-bg-w"></div><div class="fancybox-bg" id="fancybox-bg-nw"></div>').appendTo(f);
 44+D.append(j=b('<div id="fancybox-content"></div>'),E=b('<a id="fancybox-close"></a>'),n=b('<div id="fancybox-title"></div>'),z=b('<a href="javascript:;" id="fancybox-left"><span class="fancy-ico" id="fancybox-left-ico"></span></a>'),A=b('<a href="javascript:;" id="fancybox-right"><span class="fancy-ico" id="fancybox-right-ico"></span></a>'));E.click(b.fancybox.close);t.click(b.fancybox.cancel);z.click(function(a){a.preventDefault();b.fancybox.prev()});A.click(function(a){a.preventDefault();b.fancybox.next()});
 45+b.fn.mousewheel&&f.bind("mousewheel.fb",function(a,c){if(h)a.preventDefault();else if(b(a.target).get(0).clientHeight==0||b(a.target).get(0).scrollHeight===b(a.target).get(0).clientHeight){a.preventDefault();b.fancybox[c>0?"prev":"next"]()}});b.support.opacity||f.addClass("fancybox-ie");if(M){t.addClass("fancybox-ie6");f.addClass("fancybox-ie6");b('<iframe id="fancybox-hide-sel-frame" src="'+(/^https/i.test(window.location.href||"")?"javascript:void(false)":"about:blank")+'" scrolling="no" border="0" frameborder="0" tabindex="-1"></iframe>').prependTo(D)}}};
 46+b.fn.fancybox.defaults={padding:10,margin:40,opacity:false,modal:false,cyclic:false,scrolling:"auto",width:560,height:340,autoScale:true,autoDimensions:true,centerOnScroll:false,ajax:{},swf:{wmode:"transparent"},hideOnOverlayClick:true,hideOnContentClick:false,overlayShow:true,overlayOpacity:0.7,overlayColor:"#777",titleShow:true,titlePosition:"float",titleFormat:null,titleFromAlt:false,transitionIn:"fade",transitionOut:"fade",speedIn:300,speedOut:300,changeSpeed:300,changeFade:"fast",easingIn:"swing",
 47+easingOut:"swing",showCloseButton:true,showNavArrows:true,enableEscapeButton:true,enableKeyboardNav:true,onStart:function(){},onCancel:function(){},onComplete:function(){},onCleanup:function(){},onClosed:function(){},onError:function(){}};b(document).ready(function(){b.fancybox.init()})})(jQuery);
\ No newline at end of file
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/jquery.fancybox-1.3.4.pack.js
___________________________________________________________________
Added: svn:eol-style
148 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/fancy_loading.png
Cannot display: file marked as a binary type.
svn:mime-type = image/png
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/fancy_loading.png
___________________________________________________________________
Added: svn:mime-type
249 + image/png
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/fancy_title_right.png
Cannot display: file marked as a binary type.
svn:mime-type = image/png
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/fancy_title_right.png
___________________________________________________________________
Added: svn:mime-type
350 + image/png
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/jquery.mousewheel-3.0.4.pack.js
@@ -0,0 +1,14 @@
 2+/*! Copyright (c) 2010 Brandon Aaron (http://brandonaaron.net)
 3+* Licensed under the MIT License (LICENSE.txt).
 4+*
 5+* Thanks to: http://adomas.org/javascript-mouse-wheel/ for some pointers.
 6+* Thanks to: Mathias Bank(http://www.mathias-bank.de) for a scope bug fix.
 7+* Thanks to: Seamus Leahy for adding deltaX and deltaY
 8+*
 9+* Version: 3.0.4
 10+*
 11+* Requires: 1.2.2+
 12+*/
 13+
 14+(function(d){function g(a){var b=a||window.event,i=[].slice.call(arguments,1),c=0,h=0,e=0;a=d.event.fix(b);a.type="mousewheel";if(a.wheelDelta)c=a.wheelDelta/120;if(a.detail)c=-a.detail/3;e=c;if(b.axis!==undefined&&b.axis===b.HORIZONTAL_AXIS){e=0;h=-1*c}if(b.wheelDeltaY!==undefined)e=b.wheelDeltaY/120;if(b.wheelDeltaX!==undefined)h=-1*b.wheelDeltaX/120;i.unshift(a,c,h,e);return d.event.handle.apply(this,i)}var f=["DOMMouseScroll","mousewheel"];d.event.special.mousewheel={setup:function(){if(this.addEventListener)for(var a=
 15+f.length;a;)this.addEventListener(f[--a],g,false);else this.onmousewheel=g},teardown:function(){if(this.removeEventListener)for(var a=f.length;a;)this.removeEventListener(f[--a],g,false);else this.onmousewheel=null}};d.fn.extend({mousewheel:function(a){return a?this.bind("mousewheel",a):this.trigger("mousewheel")},unmousewheel:function(a){return this.unbind("mousewheel",a)}})})(jQuery);
\ No newline at end of file
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/jquery.mousewheel-3.0.4.pack.js
___________________________________________________________________
Added: svn:eol-style
116 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/fancy_nav_right.png
Cannot display: file marked as a binary type.
svn:mime-type = image/png
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/fancy_nav_right.png
___________________________________________________________________
Added: svn:mime-type
217 + image/png
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/fancy_title_main.png
Cannot display: file marked as a binary type.
svn:mime-type = image/png
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/fancy_title_main.png
___________________________________________________________________
Added: svn:mime-type
318 + image/png
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/blank.gif
Cannot display: file marked as a binary type.
svn:mime-type = image/png
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/blank.gif
___________________________________________________________________
Added: svn:mime-type
419 + image/png
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/fancy_shadow_nw.png
Cannot display: file marked as a binary type.
svn:mime-type = image/png
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/fancy_shadow_nw.png
___________________________________________________________________
Added: svn:mime-type
520 + image/png
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/fancy_shadow_e.png
Cannot display: file marked as a binary type.
svn:mime-type = image/png
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/fancy_shadow_e.png
___________________________________________________________________
Added: svn:mime-type
621 + image/png
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/fancybox.png
Cannot display: file marked as a binary type.
svn:mime-type = image/png
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/fancybox.png
___________________________________________________________________
Added: svn:mime-type
722 + image/png
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/fancy_title_left.png
Cannot display: file marked as a binary type.
svn:mime-type = image/png
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/fancybox/fancy_title_left.png
___________________________________________________________________
Added: svn:mime-type
823 + image/png
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/contest.special.contests.js
@@ -0,0 +1,57 @@
 2+/**
 3+ * JavasSript for the Contest MediaWiki extension.
 4+ * @see https://www.mediawiki.org/wiki/Extension:Contest
 5+ *
 6+ * @licence GNU GPL v3 or later
 7+ * @author Jeroen De Dauw <jeroendedauw at gmail dot com>
 8+ */
 9+
 10+(function( $, mw ) { $( document ).ready( function() {
 11+
 12+ function deleteContest( options, successCallback, failCallback ) {
 13+ $.post(
 14+ wgScriptPath + '/api.php',
 15+ {
 16+ 'action': 'deletecontest',
 17+ 'format': 'json',
 18+ 'ids': options.id,
 19+ 'token': options.token
 20+ },
 21+ function( data ) {
 22+ if ( data.success ) {
 23+ successCallback();
 24+ } else {
 25+ failCallback( mw.msg( 'contest-special-delete-failed' ) );
 26+ }
 27+ }
 28+ );
 29+ }
 30+
 31+ $( '.contest-delete' ).click( function() {
 32+ $this = $( this );
 33+
 34+ if ( confirm( mw.msg( 'contest-special-confirm-delete' ) ) ) {
 35+ deleteContest(
 36+ {
 37+ id: $this.attr( 'data-contest-id' ),
 38+ token: $this.attr( 'data-contest-token' )
 39+ },
 40+ function() {
 41+ $this.closest( 'tr' ).slideUp( 'slow', function() {
 42+ $( this ).remove();
 43+
 44+ if ( $( '.contests-table tr' ).length < 2 ) {
 45+ $( '.contests-table' ).remove();
 46+ $( '.contests-title' ).remove();
 47+ }
 48+ } );
 49+ },
 50+ function( error ) {
 51+ alert( error );
 52+ }
 53+ );
 54+ }
 55+ return false;
 56+ } );
 57+
 58+} ); })( window.jQuery, window.mediaWiki );
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/contest.special.contests.js
___________________________________________________________________
Added: svn:eol-style
159 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/contest.special.editcontest.js
@@ -0,0 +1,173 @@
 2+/**
 3+ * JavasSript for the Contest MediaWiki extension.
 4+ * @see https://www.mediawiki.org/wiki/Extension:Contest
 5+ *
 6+ * @licence GNU GPL v3 or later
 7+ * @author Jeroen De Dauw <jeroendedauw at gmail dot com>
 8+ */
 9+
 10+(function( $, mw ) {
 11+
 12+ function addChallengeToRemove( id ) {
 13+ if ( !isNaN( id ) ) {
 14+ var currentVal = $( '#delete-challenges' ).val();
 15+
 16+ var currentIds = currentVal !== '' ? currentVal.split( '|' ) : [];
 17+ currentIds.push( id );
 18+
 19+ $( '#delete-challenges' ).val( currentIds.join( '|' ) );
 20+ }
 21+ }
 22+
 23+ $.fn.mwChallenge = function( options ) {
 24+
 25+ var _this = this;
 26+ var $this = $( this );
 27+ this.options = options;
 28+
 29+ this.titleInput = null;
 30+ this.textInput = null;
 31+ this.deleteButton = null;
 32+
 33+ this.remove = function() {
 34+ addChallengeToRemove( $this.attr( 'data-challenge-id' ) );
 35+
 36+ $tr = $this.closest( 'tr' );
 37+ $tr.slideUp( 'fast', function() { $tr.remove(); } );
 38+ };
 39+
 40+ this.init = function() {
 41+ $this.html( '' );
 42+
 43+ this.titleInput = $( '<input />' ).attr( {
 44+ 'type': 'text',
 45+ 'name': 'contest-challenge-' + $this.attr( 'data-challenge-id' ),
 46+ 'size': 45
 47+ } ).val( $this.attr( 'data-challenge-title' ) );
 48+
 49+ $this.append(
 50+ $( '<div />' ).html(
 51+ $( '<label />' )
 52+ .text( mw.msg( 'contest-edit-challenge-title' ) )
 53+ .attr( 'for', 'contest-challenge-' + $this.attr( 'data-challenge-id' ) )
 54+ ).append( '&#160;' ).append( this.titleInput )
 55+ );
 56+
 57+ this.onelineInput = $( '<input />' ).attr( {
 58+ 'type': 'text',
 59+ 'name': 'challenge-oneline-' + $this.attr( 'data-challenge-id' ),
 60+ 'size': 45,
 61+ 'style': 'margin-top: 3px'
 62+ } ).val( $this.attr( 'data-challenge-oneline' ) );
 63+
 64+ $this.append(
 65+ $( '<div />' ).html(
 66+ $( '<label />' )
 67+ .text( mw.msg( 'contest-edit-challenge-oneline' ) )
 68+ .attr( { 'for': 'contest-oneline-' + $this.attr( 'data-challenge-id' ) } )
 69+ ).append( '&#160;' ).append( this.onelineInput )
 70+ );
 71+
 72+ this.textInput = $( '<textarea />' ).attr( {
 73+ 'name': 'challenge-text-' + $this.attr( 'data-challenge-id' )
 74+ } ).val( $this.attr( 'data-challenge-text' ) );
 75+
 76+ $this.append(
 77+ $( '<div />' ).html(
 78+ $( '<label />' )
 79+ .text( mw.msg( 'contest-edit-challenge-text' ) )
 80+ .attr( 'for', 'challenge-text-' + $this.attr( 'data-challenge-id' ) )
 81+ ).append( '<br />' ).append( this.textInput )
 82+ );
 83+
 84+ this.deleteButton = $( '<button />' )
 85+ .button( { 'label': mw.msg( 'contest-edit-delete' ) } )
 86+ .click( function() {
 87+ if ( confirm( mw.msg( 'contest-edit-confirm-delete' ) ) ) {
 88+ _this.remove();
 89+ return false;
 90+ }
 91+ } );
 92+
 93+ $this.append( this.deleteButton );
 94+ };
 95+
 96+ this.init();
 97+
 98+ return this;
 99+
 100+ };
 101+
 102+ var newNr = 0;
 103+ var $table = null;
 104+
 105+ function getNewChallengeMessage() {
 106+ return mw.msg( 'contest-edit-add-' + ( $( '.contest-challenge-input' ).size() === 0 ? 'first' : 'another' ) );
 107+ }
 108+
 109+ function addChallenge( challenge ) {
 110+ $challenge = $( '<div />' ).attr( {
 111+ 'class': 'contest-challenge-input',
 112+ 'data-challenge-id': challenge.id,
 113+ 'data-challenge-title': challenge.title,
 114+ 'data-challenge-text': challenge.text,
 115+ 'data-challenge-oneline': challenge.oneline
 116+ } );
 117+
 118+ $tr = $( '<tr />' );
 119+
 120+ $tr.append( $( '<td />' ) );
 121+
 122+ $tr.append( $( '<td />' ).html( $challenge ).append( '<hr />' ) );
 123+
 124+ $( '.add-new-challenge' ).before( $tr );
 125+
 126+ $challenge.mwChallenge();
 127+ }
 128+
 129+ $( document ).ready( function() {
 130+
 131+ $( '#cancelEdit' ).click( function() {
 132+ window.location = $( this ).attr( 'target-url' );
 133+ } );
 134+
 135+ $table = $( '#contest-name-field' ).closest( 'tbody' );
 136+
 137+ $( '#bodyContent' ).find( '[type="submit"]' ).button();
 138+
 139+ $table.append( '<tr><td colspan="2"><hr /></td></tr>' );
 140+
 141+ $addNew = $( '<button />' ).button( { 'label': getNewChallengeMessage() } ).click( function() {
 142+ addChallenge( {
 143+ 'id': 'new-' + newNr++ ,
 144+ 'title': '',
 145+ 'text': ''
 146+ } );
 147+
 148+ $( this ).button( { 'label': getNewChallengeMessage() } );
 149+
 150+ return false;
 151+ } );
 152+
 153+ $table.append( $( '<tr />' ).attr( 'class', 'add-new-challenge' ).html( $( '<td />' ) ).append( $( '<td />' ).html( $addNew ) ) );
 154+
 155+ $table.append( '<tr><td colspan="2"><hr /></td></tr>' );
 156+
 157+ $( '.contest-challenge' ).each( function( index, domElement ) {
 158+ $this = $( domElement );
 159+ addChallenge( {
 160+ 'id': $this.attr( 'data-challenge-id' ),
 161+ 'title': $this.attr( 'data-challenge-title' ),
 162+ 'text': $this.attr( 'data-challenge-text' ),
 163+ 'oneline': $this.attr( 'data-challenge-oneline' ),
 164+ } );
 165+ } );
 166+
 167+ $( '#contest-edit-end' ).datetimepicker( {
 168+ minDate: new Date(),
 169+ dateFormat: 'yy-mm-dd'
 170+ } );
 171+
 172+ } );
 173+
 174+})( window.jQuery, window.mediaWiki );
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/contest.special.editcontest.js
___________________________________________________________________
Added: svn:eol-style
1175 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/contest.special.signup.js
@@ -0,0 +1,39 @@
 2+/**
 3+ * JavasSript for the Contest MediaWiki extension.
 4+ * @see https://www.mediawiki.org/wiki/Extension:Contest
 5+ *
 6+ * @licence GNU GPL v3 or later
 7+ * @author Jeroen De Dauw <jeroendedauw at gmail dot com>
 8+ */
 9+
 10+(function( $, mw ) {
 11+
 12+ $( document ).ready( function() {
 13+
 14+ var contestConfig = mw.config.get( 'ContestConfig' );
 15+
 16+ $( '.mw-htmlform-submit' ).button();
 17+
 18+ $rules = $( '#contest-rules' );
 19+
 20+ $div = $( '<div />' ).attr( {
 21+ 'style': 'display:none'
 22+ } ).html( $( '<div />' ).attr( { 'id': 'contest-rules-div' } ).html( contestConfig['rules_page'] ) );
 23+
 24+ $a = $( "label[for='contest-rules']" ).find( 'a' );
 25+ $a.attr( { 'href': '#contest-rules-div' } );
 26+
 27+ $rules.closest( 'td' ).append( $div );
 28+
 29+ $a.fancybox( {
 30+ 'width' : '85%',
 31+ 'height' : '85%',
 32+ 'transitionIn' : 'none',
 33+ 'transitionOut' : 'none',
 34+ 'type' : 'inline',
 35+ 'autoDimensions': false
 36+ } );
 37+
 38+ } );
 39+
 40+})( window.jQuery, window.mediaWiki );
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/contest.special.signup.js
___________________________________________________________________
Added: svn:eol-style
141 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/jquery.ui.timepicker.css
@@ -0,0 +1,6 @@
 2+/* css for timepicker */
 3+.ui-timepicker-div .ui-widget-header{ margin-bottom: 8px; }
 4+.ui-timepicker-div dl{ text-align: left; }
 5+.ui-timepicker-div dl dt{ height: 25px; }
 6+.ui-timepicker-div dl dd{ margin: -25px 0 10px 65px; }
 7+.ui-timepicker-div td { font-size: 90%; }
\ No newline at end of file
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/jquery.ui.timepicker.css
___________________________________________________________________
Added: svn:eol-style
18 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/jquery.contestChallenges.js
@@ -0,0 +1,100 @@
 2+/**
 3+ * JavasSript for the Contest MediaWiki extension.
 4+ * @see https://www.mediawiki.org/wiki/Extension:Contest
 5+ *
 6+ * @licence GNU GPL v3 or later
 7+ * @author Jeroen De Dauw <jeroendedauw at gmail dot com>
 8+ */
 9+
 10+( function ( $, mw ) { $.fn.contestChallenges = function( challenges, config ) {
 11+
 12+ this.challenges = challenges;
 13+ this.config = config;
 14+
 15+ var _this = this;
 16+ var $this = $( this );
 17+
 18+ this.challengesList = null;
 19+
 20+ this.addChallenge = function( challenge ) {
 21+ this.challengesList
 22+ .append(
 23+ $( '<li class="mw-codechallenge-box-outside"></li>' )
 24+ .click( function( e ) {
 25+ var box = $(this);
 26+ if ( !box.hasClass( 'mw-codechallenge-box-selected' ) ) {
 27+ $( '.mw-codechallenge-popup' ).not( box ).fadeOut( 'fast' );
 28+ $( '.mw-codechallenge-box-selected' ).removeClass( 'mw-codechallenge-box-selected' );
 29+ box
 30+ .addClass( 'mw-codechallenge-box-selected' )
 31+ .find( '.mw-codechallenge-popup' )
 32+ .fadeIn( 'fast' );
 33+ $(document).one( 'click', function() {
 34+ box
 35+ .removeClass( 'mw-codechallenge-box-selected' )
 36+ .find( '.mw-codechallenge-popup' )
 37+ .fadeOut( 'fast' );
 38+ } );
 39+ e.stopPropagation();
 40+ return false;
 41+ }
 42+ } )
 43+ .append(
 44+ $( '<div class="mw-codechallenge-box-inside"></div>' )
 45+ .append( '<div class="mw-codechallenge-box-top"></div>' )
 46+ .append(
 47+ $('<div class="mw-codechallenge-box-text"></div>' )
 48+ .append(
 49+ $( '<h4 class="mw-codechallenge-box-title"></h4>' )
 50+ .text( challenge.title )
 51+ )
 52+ .append(
 53+ $( '<p class="mw-codechallenge-box-desc">' )
 54+ .text( challenge.oneline )
 55+ )
 56+ )
 57+ .append(
 58+ $( '<div class="mw-codechallenge-popup"><div>' )
 59+ .click( function() {
 60+ return false;
 61+ } )
 62+ .append( '<div class="mw-codechallenge-popup-callout"></div>' )
 63+ .append( challenge.text )
 64+ .append(
 65+ $( '<div class="mw-codechallenge-popup-buttons"></div>' )
 66+ .append(
 67+ $( '<button class="ui-button-green"></button>' )
 68+ // TODO: Internationalize this!
 69+ .text( mw.msg( 'contest-welcome-accept-challenge' ) )
 70+ .button()
 71+ .click( function() {
 72+ window.location = challenge.target;
 73+ } )
 74+ )
 75+ )
 76+ )
 77+ )
 78+ );
 79+ }
 80+
 81+ this.initChallenges = function() {
 82+ this.challengesList = $( '<ul />' ).attr( 'id', 'contest-challenges-list' );
 83+
 84+ for ( var i in this.challenges ) {
 85+ this.addChallenge( this.challenges[i] );
 86+ }
 87+ };
 88+
 89+ this.init = function() {
 90+ $this.html( $( '<h3 />' ).text( mw.msg( 'contest-welcome-select-header' ) ) );
 91+
 92+ this.initChallenges();
 93+
 94+ $this.append( this.challengesList );
 95+ };
 96+
 97+ this.init();
 98+
 99+ return this;
 100+
 101+}; } )( window.jQuery, window.mediaWiki );
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/jquery.contestChallenges.js
___________________________________________________________________
Added: svn:eol-style
1102 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/contest.special.welcome.css
@@ -0,0 +1,141 @@
 2+/**
 3+ * CSS for the Contest MediaWiki extension.
 4+ * @see https://www.mediawiki.org/wiki/Extension:Contest
 5+ *
 6+ * @licence GNU GPL v3 or later
 7+ * @author Jeroen De Dauw <jeroendedauw at gmail dot com>
 8+ */
 9+
 10+/* dialog type selection */
 11+#contest-challenges-list {
 12+ float: left;
 13+ width: 354px;
 14+ margin: 1.5em 0 0.5em 0;
 15+ margin-top: 1.5em;
 16+}
 17+#contest-challenges-list ul {
 18+ list-style: none;
 19+ list-style-image: none;
 20+ margin: 0;
 21+ padding: 0;
 22+}
 23+/* IGNORED BY IE6 */
 24+#contest-challenges-list ul > li {
 25+ display: inline-block;
 26+}
 27+.mw-codechallenge-box-outside {
 28+ list-style: none;
 29+ list-style-image: none;
 30+ position: relative;
 31+ display: block;
 32+ width: 354px;
 33+ padding: 0;
 34+ margin: 0 0 1em 0;
 35+ /* @embed */
 36+ background-image: url(images/box-sprite.png);
 37+ background-position: 0 0;
 38+ background-repeat: repeat-y;
 39+ cursor: pointer;
 40+}
 41+.mw-codechallenge-box-outside:hover,
 42+.mw-codechallenge-box-outside:focus {
 43+ background-position: -354px 0;
 44+}
 45+.mw-codechallenge-box-selected {
 46+ background-position: -708px 0 !important;
 47+}
 48+.mw-codechallenge-box-outside .mw-codechallenge-box-inside {
 49+ /* @embed */
 50+ background-image: url(images/box-bottom-sprite.png);
 51+ background-position: 0 bottom;
 52+ background-repeat: no-repeat;
 53+}
 54+.mw-codechallenge-box-outside:hover .mw-codechallenge-box-inside,
 55+.mw-codechallenge-box-outside:focus .mw-codechallenge-box-inside {
 56+ background-position: -354px bottom;
 57+}
 58+.mw-codechallenge-box-selected .mw-codechallenge-box-inside {
 59+ background-position: -708px bottom !important;
 60+}
 61+.mw-codechallenge-box-outside .mw-codechallenge-box-top {
 62+ height: 10px;
 63+ /* @embed */
 64+ background-image: url(images/box-top-sprite.png);
 65+ background-position: 0 top;
 66+ background-repeat: no-repeat;
 67+}
 68+.mw-codechallenge-box-outside:hover .mw-codechallenge-box-top,
 69+.mw-codechallenge-box-outside:focus .mw-codechallenge-box-top {
 70+ background-position: -354px top;
 71+}
 72+.mw-codechallenge-box-selected .mw-codechallenge-box-top {
 73+ background-position: -708px top !important;
 74+}
 75+.mw-codechallenge-box-text {
 76+ display: block;
 77+ margin-top: -10px;
 78+ padding: 1em 1.5em 1.25em 1.5em;
 79+ text-decoration: none;
 80+ outline: none;
 81+ /* @embed */
 82+ background-image: url(images/arrow.png);
 83+ background-position: right center;
 84+ background-repeat: no-repeat;
 85+}
 86+.mw-codechallenge-box-outside:hover .mw-codechallenge-box-text,
 87+.mw-codechallenge-box-outside:focus .mw-codechallenge-box-text,
 88+.mw-codechallenge-box-selected .mw-codechallenge-box-text {
 89+ /* @embed */
 90+ background-image: url(images/arrow-hover.png);
 91+ text-decoration:none;
 92+}
 93+.mw-codechallenge-box-title {
 94+ color: white;
 95+ margin: 0;
 96+ padding: 0 32px 0 0;
 97+}
 98+.mw-codechallenge-box-desc {
 99+ color: white;
 100+ margin: 0;
 101+ padding: 0 32px 0 0;
 102+}
 103+.mw-codechallenge-popup {
 104+ display: none;
 105+ position: absolute;
 106+ left: 354px;
 107+ top: 50%;
 108+ margin-top: -22px;
 109+ width: 354px;
 110+ padding: 1.25em;
 111+ border: solid 3px #3263b6;
 112+ -webkit-box-shadow: 2px 2px 10px 0px rgba(0, 0, 0, 0.5);
 113+ -moz-box-shadow: 2px 2px 10px 0px rgba(0, 0, 0, 0.5);
 114+ box-shadow: 2px 2px 10px 0px rgba(0, 0, 0, 0.5);
 115+ background-color: white;
 116+ cursor: default;
 117+}
 118+.mw-codechallenge-popup-callout {
 119+ position: absolute;
 120+ left: -16px;
 121+ top: 3px;
 122+ width: 16px;
 123+ height: 32px;
 124+ /* @embed */
 125+ background-image: url(images/callout.png);
 126+ background-position: left top;
 127+ background-repeat: no-repeat;
 128+}
 129+.mw-codechallenge-popup-buttons {
 130+ margin-top: 1em;
 131+ margin-bottom: -0.5em;
 132+ text-align: right;
 133+}
 134+
 135+
 136+
 137+
 138+
 139+
 140+
 141+
 142+
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/contest.special.welcome.css
___________________________________________________________________
Added: svn:eol-style
1143 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/resources/jquery.ui.timepicker.js
@@ -0,0 +1,1276 @@
 2+/*
 3+* jQuery timepicker addon
 4+* By: Trent Richardson [http://trentrichardson.com]
 5+* Version 0.9.7
 6+* Last Modified: 10/02/2011
 7+*
 8+* Copyright 2011 Trent Richardson
 9+* Dual licensed under the MIT and GPL licenses.
 10+* http://trentrichardson.com/Impromptu/GPL-LICENSE.txt
 11+* http://trentrichardson.com/Impromptu/MIT-LICENSE.txt
 12+*
 13+* HERES THE CSS:
 14+* .ui-timepicker-div .ui-widget-header { margin-bottom: 8px; }
 15+* .ui-timepicker-div dl { text-align: left; }
 16+* .ui-timepicker-div dl dt { height: 25px; }
 17+* .ui-timepicker-div dl dd { margin: -25px 10px 10px 65px; }
 18+* .ui-timepicker-div td { font-size: 90%; }
 19+* .ui-tpicker-grid-label { background: none; border: none; margin: 0; padding: 0; }
 20+*/
 21+
 22+(function($) {
 23+
 24+$.extend($.ui, { timepicker: { version: "0.9.7" } });
 25+
 26+/* Time picker manager.
 27+ Use the singleton instance of this class, $.timepicker, to interact with the time picker.
 28+ Settings for (groups of) time pickers are maintained in an instance object,
 29+ allowing multiple different settings on the same page. */
 30+
 31+function Timepicker() {
 32+ this.regional = []; // Available regional settings, indexed by language code
 33+ this.regional[''] = { // Default regional settings
 34+ currentText: 'Now',
 35+ closeText: 'Done',
 36+ ampm: false,
 37+ amNames: ['AM', 'A'],
 38+ pmNames: ['PM', 'P'],
 39+ timeFormat: 'hh:mm tt',
 40+ timeSuffix: '',
 41+ timeOnlyTitle: 'Choose Time',
 42+ timeText: 'Time',
 43+ hourText: 'Hour',
 44+ minuteText: 'Minute',
 45+ secondText: 'Second',
 46+ millisecText: 'Millisecond',
 47+ timezoneText: 'Time Zone'
 48+ };
 49+ this._defaults = { // Global defaults for all the datetime picker instances
 50+ showButtonPanel: true,
 51+ timeOnly: false,
 52+ showHour: true,
 53+ showMinute: true,
 54+ showSecond: false,
 55+ showMillisec: false,
 56+ showTimezone: false,
 57+ showTime: true,
 58+ stepHour: 0.05,
 59+ stepMinute: 0.05,
 60+ stepSecond: 0.05,
 61+ stepMillisec: 0.5,
 62+ hour: 0,
 63+ minute: 0,
 64+ second: 0,
 65+ millisec: 0,
 66+ timezone: '+0000',
 67+ hourMin: 0,
 68+ minuteMin: 0,
 69+ secondMin: 0,
 70+ millisecMin: 0,
 71+ hourMax: 23,
 72+ minuteMax: 59,
 73+ secondMax: 59,
 74+ millisecMax: 999,
 75+ minDateTime: null,
 76+ maxDateTime: null,
 77+ onSelect: null,
 78+ hourGrid: 0,
 79+ minuteGrid: 0,
 80+ secondGrid: 0,
 81+ millisecGrid: 0,
 82+ alwaysSetTime: true,
 83+ separator: ' ',
 84+ altFieldTimeOnly: true,
 85+ showTimepicker: true,
 86+ timezoneIso8609: false,
 87+ timezoneList: null
 88+ };
 89+ $.extend(this._defaults, this.regional['']);
 90+}
 91+
 92+$.extend(Timepicker.prototype, {
 93+ $input: null,
 94+ $altInput: null,
 95+ $timeObj: null,
 96+ inst: null,
 97+ hour_slider: null,
 98+ minute_slider: null,
 99+ second_slider: null,
 100+ millisec_slider: null,
 101+ timezone_select: null,
 102+ hour: 0,
 103+ minute: 0,
 104+ second: 0,
 105+ millisec: 0,
 106+ timezone: '+0000',
 107+ hourMinOriginal: null,
 108+ minuteMinOriginal: null,
 109+ secondMinOriginal: null,
 110+ millisecMinOriginal: null,
 111+ hourMaxOriginal: null,
 112+ minuteMaxOriginal: null,
 113+ secondMaxOriginal: null,
 114+ millisecMaxOriginal: null,
 115+ ampm: '',
 116+ formattedDate: '',
 117+ formattedTime: '',
 118+ formattedDateTime: '',
 119+ timezoneList: null,
 120+
 121+ /* Override the default settings for all instances of the time picker.
 122+ @param settings object - the new settings to use as defaults (anonymous object)
 123+ @return the manager object */
 124+ setDefaults: function(settings) {
 125+ extendRemove(this._defaults, settings || {});
 126+ return this;
 127+ },
 128+
 129+ //########################################################################
 130+ // Create a new Timepicker instance
 131+ //########################################################################
 132+ _newInst: function($input, o) {
 133+ var tp_inst = new Timepicker(),
 134+ inlineSettings = {};
 135+
 136+ for (var attrName in this._defaults) {
 137+ var attrValue = $input.attr('time:' + attrName);
 138+ if (attrValue) {
 139+ try {
 140+ inlineSettings[attrName] = eval(attrValue);
 141+ } catch (err) {
 142+ inlineSettings[attrName] = attrValue;
 143+ }
 144+ }
 145+ }
 146+ tp_inst._defaults = $.extend({}, this._defaults, inlineSettings, o, {
 147+ beforeShow: function(input, dp_inst) {
 148+ if ($.isFunction(o.beforeShow))
 149+ o.beforeShow(input, dp_inst, tp_inst);
 150+ },
 151+ onChangeMonthYear: function(year, month, dp_inst) {
 152+ // Update the time as well : this prevents the time from disappearing from the $input field.
 153+ tp_inst._updateDateTime(dp_inst);
 154+ if ($.isFunction(o.onChangeMonthYear))
 155+ o.onChangeMonthYear.call($input[0], year, month, dp_inst, tp_inst);
 156+ },
 157+ onClose: function(dateText, dp_inst) {
 158+ if (tp_inst.timeDefined === true && $input.val() != '')
 159+ tp_inst._updateDateTime(dp_inst);
 160+ if ($.isFunction(o.onClose))
 161+ o.onClose.call($input[0], dateText, dp_inst, tp_inst);
 162+ },
 163+ timepicker: tp_inst // add timepicker as a property of datepicker: $.datepicker._get(dp_inst, 'timepicker');
 164+ });
 165+ tp_inst.amNames = $.map(tp_inst._defaults.amNames, function(val) { return val.toUpperCase() });
 166+ tp_inst.pmNames = $.map(tp_inst._defaults.pmNames, function(val) { return val.toUpperCase() });
 167+
 168+ if (tp_inst._defaults.timezoneList === null) {
 169+ var timezoneList = [];
 170+ for (var i = -11; i <= 12; i++)
 171+ timezoneList.push((i >= 0 ? '+' : '-') + ('0' + Math.abs(i).toString()).slice(-2) + '00');
 172+ if (tp_inst._defaults.timezoneIso8609)
 173+ timezoneList = $.map(timezoneList, function(val) {
 174+ return val == '+0000' ? 'Z' : (val.substring(0, 3) + ':' + val.substring(3));
 175+ });
 176+ tp_inst._defaults.timezoneList = timezoneList;
 177+ }
 178+
 179+ tp_inst.hour = tp_inst._defaults.hour;
 180+ tp_inst.minute = tp_inst._defaults.minute;
 181+ tp_inst.second = tp_inst._defaults.second;
 182+ tp_inst.millisec = tp_inst._defaults.millisec;
 183+ tp_inst.ampm = '';
 184+ tp_inst.$input = $input;
 185+
 186+ if (o.altField)
 187+ tp_inst.$altInput = $(o.altField)
 188+ .css({ cursor: 'pointer' })
 189+ .focus(function(){ $input.trigger("focus"); });
 190+
 191+ if(tp_inst._defaults.minDate==0 || tp_inst._defaults.minDateTime==0)
 192+ {
 193+ tp_inst._defaults.minDate=new Date();
 194+ }
 195+ if(tp_inst._defaults.maxDate==0 || tp_inst._defaults.maxDateTime==0)
 196+ {
 197+ tp_inst._defaults.maxDate=new Date();
 198+ }
 199+
 200+ // datepicker needs minDate/maxDate, timepicker needs minDateTime/maxDateTime..
 201+ if(tp_inst._defaults.minDate !== undefined && tp_inst._defaults.minDate instanceof Date)
 202+ tp_inst._defaults.minDateTime = new Date(tp_inst._defaults.minDate.getTime());
 203+ if(tp_inst._defaults.minDateTime !== undefined && tp_inst._defaults.minDateTime instanceof Date)
 204+ tp_inst._defaults.minDate = new Date(tp_inst._defaults.minDateTime.getTime());
 205+ if(tp_inst._defaults.maxDate !== undefined && tp_inst._defaults.maxDate instanceof Date)
 206+ tp_inst._defaults.maxDateTime = new Date(tp_inst._defaults.maxDate.getTime());
 207+ if(tp_inst._defaults.maxDateTime !== undefined && tp_inst._defaults.maxDateTime instanceof Date)
 208+ tp_inst._defaults.maxDate = new Date(tp_inst._defaults.maxDateTime.getTime());
 209+ return tp_inst;
 210+ },
 211+
 212+ //########################################################################
 213+ // add our sliders to the calendar
 214+ //########################################################################
 215+ _addTimePicker: function(dp_inst) {
 216+ var currDT = (this.$altInput && this._defaults.altFieldTimeOnly) ?
 217+ this.$input.val() + ' ' + this.$altInput.val() :
 218+ this.$input.val();
 219+
 220+ this.timeDefined = this._parseTime(currDT);
 221+ this._limitMinMaxDateTime(dp_inst, false);
 222+ this._injectTimePicker();
 223+ },
 224+
 225+ //########################################################################
 226+ // parse the time string from input value or _setTime
 227+ //########################################################################
 228+ _parseTime: function(timeString, withDate) {
 229+ var regstr = this._defaults.timeFormat.toString()
 230+ .replace(/h{1,2}/ig, '(\\d?\\d)')
 231+ .replace(/m{1,2}/ig, '(\\d?\\d)')
 232+ .replace(/s{1,2}/ig, '(\\d?\\d)')
 233+ .replace(/l{1}/ig, '(\\d?\\d?\\d)')
 234+ .replace(/t{1,2}/ig, this._getPatternAmpm())
 235+ .replace(/z{1}/ig, '(z|[-+]\\d\\d:?\\d\\d)?')
 236+ .replace(/\s/g, '\\s?') + this._defaults.timeSuffix + '$',
 237+ order = this._getFormatPositions(),
 238+ ampm = '',
 239+ treg;
 240+
 241+ if (!this.inst) this.inst = $.datepicker._getInst(this.$input[0]);
 242+
 243+ if (withDate || !this._defaults.timeOnly) {
 244+ // the time should come after x number of characters and a space.
 245+ // x = at least the length of text specified by the date format
 246+ var dp_dateFormat = $.datepicker._get(this.inst, 'dateFormat');
 247+ // escape special regex characters in the seperator
 248+ var specials = new RegExp("[.*+?|()\\[\\]{}\\\\]", "g");
 249+ regstr = '.{' + dp_dateFormat.length + ',}' + this._defaults.separator.replace(specials, "\\$&") + regstr;
 250+ }
 251+
 252+ treg = timeString.match(new RegExp(regstr, 'i'));
 253+
 254+ if (treg) {
 255+ if (order.t !== -1) {
 256+ if (treg[order.t] === undefined || treg[order.t].length === 0) {
 257+ ampm = '';
 258+ this.ampm = '';
 259+ } else {
 260+ ampm = $.inArray(treg[order.t].toUpperCase(), this.amNames) !== -1 ? 'AM' : 'PM';
 261+ this.ampm = this._defaults[ampm == 'AM' ? 'amNames' : 'pmNames'][0];
 262+ }
 263+ }
 264+
 265+ if (order.h !== -1) {
 266+ if (ampm == 'AM' && treg[order.h] == '12')
 267+ this.hour = 0; // 12am = 0 hour
 268+ else if (ampm == 'PM' && treg[order.h] != '12')
 269+ this.hour = (parseFloat(treg[order.h]) + 12).toFixed(0); // 12pm = 12 hour, any other pm = hour + 12
 270+ else this.hour = Number(treg[order.h]);
 271+ }
 272+
 273+ if (order.m !== -1) this.minute = Number(treg[order.m]);
 274+ if (order.s !== -1) this.second = Number(treg[order.s]);
 275+ if (order.l !== -1) this.millisec = Number(treg[order.l]);
 276+ if (order.z !== -1 && treg[order.z] !== undefined) {
 277+ var tz = treg[order.z].toUpperCase();
 278+ switch (tz.length) {
 279+ case 1: // Z
 280+ tz = this._defaults.timezoneIso8609 ? 'Z' : '+0000';
 281+ break;
 282+ case 5: // +hhmm
 283+ if (this._defaults.timezoneIso8609)
 284+ tz = tz.substring(1) == '0000'
 285+ ? 'Z'
 286+ : tz.substring(0, 3) + ':' + tz.substring(3);
 287+ break;
 288+ case 6: // +hh:mm
 289+ if (!this._defaults.timezoneIso8609)
 290+ tz = tz == 'Z' || tz.substring(1) == '00:00'
 291+ ? '+0000'
 292+ : tz.replace(/:/, '');
 293+ else if (tz.substring(1) == '00:00')
 294+ tz = 'Z';
 295+ break;
 296+ }
 297+ this.timezone = tz;
 298+ }
 299+
 300+ return true;
 301+
 302+ }
 303+ return false;
 304+ },
 305+
 306+ //########################################################################
 307+ // pattern for standard and localized AM/PM markers
 308+ //########################################################################
 309+ _getPatternAmpm: function() {
 310+ var markers = [];
 311+ o = this._defaults;
 312+ if (o.amNames)
 313+ $.merge(markers, o.amNames);
 314+ if (o.pmNames)
 315+ $.merge(markers, o.pmNames);
 316+ markers = $.map(markers, function(val) { return val.replace(/[.*+?|()\[\]{}\\]/g, '\\$&') });
 317+ return '(' + markers.join('|') + ')?';
 318+ },
 319+
 320+ //########################################################################
 321+ // figure out position of time elements.. cause js cant do named captures
 322+ //########################################################################
 323+ _getFormatPositions: function() {
 324+ var finds = this._defaults.timeFormat.toLowerCase().match(/(h{1,2}|m{1,2}|s{1,2}|l{1}|t{1,2}|z)/g),
 325+ orders = { h: -1, m: -1, s: -1, l: -1, t: -1, z: -1 };
 326+
 327+ if (finds)
 328+ for (var i = 0; i < finds.length; i++)
 329+ if (orders[finds[i].toString().charAt(0)] == -1)
 330+ orders[finds[i].toString().charAt(0)] = i + 1;
 331+
 332+ return orders;
 333+ },
 334+
 335+ //########################################################################
 336+ // generate and inject html for timepicker into ui datepicker
 337+ //########################################################################
 338+ _injectTimePicker: function() {
 339+ var $dp = this.inst.dpDiv,
 340+ o = this._defaults,
 341+ tp_inst = this,
 342+ // Added by Peter Medeiros:
 343+ // - Figure out what the hour/minute/second max should be based on the step values.
 344+ // - Example: if stepMinute is 15, then minMax is 45.
 345+ hourMax = (o.hourMax - ((o.hourMax - o.hourMin) % o.stepHour)).toFixed(0),
 346+ minMax = (o.minuteMax - ((o.minuteMax - o.minuteMin) % o.stepMinute)).toFixed(0),
 347+ secMax = (o.secondMax - ((o.secondMax - o.secondMin) % o.stepSecond)).toFixed(0),
 348+ millisecMax = (o.millisecMax - ((o.millisecMax - o.millisecMin) % o.stepMillisec)).toFixed(0),
 349+ dp_id = this.inst.id.toString().replace(/([^A-Za-z0-9_])/g, '');
 350+
 351+ // Prevent displaying twice
 352+ //if ($dp.find("div#ui-timepicker-div-"+ dp_id).length === 0) {
 353+ if ($dp.find("div#ui-timepicker-div-"+ dp_id).length === 0 && o.showTimepicker) {
 354+ var noDisplay = ' style="display:none;"',
 355+ html = '<div class="ui-timepicker-div" id="ui-timepicker-div-' + dp_id + '"><dl>' +
 356+ '<dt class="ui_tpicker_time_label" id="ui_tpicker_time_label_' + dp_id + '"' +
 357+ ((o.showTime) ? '' : noDisplay) + '>' + o.timeText + '</dt>' +
 358+ '<dd class="ui_tpicker_time" id="ui_tpicker_time_' + dp_id + '"' +
 359+ ((o.showTime) ? '' : noDisplay) + '></dd>' +
 360+ '<dt class="ui_tpicker_hour_label" id="ui_tpicker_hour_label_' + dp_id + '"' +
 361+ ((o.showHour) ? '' : noDisplay) + '>' + o.hourText + '</dt>',
 362+ hourGridSize = 0,
 363+ minuteGridSize = 0,
 364+ secondGridSize = 0,
 365+ millisecGridSize = 0,
 366+ size;
 367+
 368+ // Hours
 369+ if (o.showHour && o.hourGrid > 0) {
 370+ html += '<dd class="ui_tpicker_hour">' +
 371+ '<div id="ui_tpicker_hour_' + dp_id + '"' + ((o.showHour) ? '' : noDisplay) + '></div>' +
 372+ '<div style="padding-left: 1px"><table class="ui-tpicker-grid-label"><tr>';
 373+
 374+ for (var h = o.hourMin; h <= hourMax; h += parseInt(o.hourGrid,10)) {
 375+ hourGridSize++;
 376+ var tmph = (o.ampm && h > 12) ? h-12 : h;
 377+ if (tmph < 10) tmph = '0' + tmph;
 378+ if (o.ampm) {
 379+ if (h == 0) tmph = 12 +'a';
 380+ else if (h < 12) tmph += 'a';
 381+ else tmph += 'p';
 382+ }
 383+ html += '<td>' + tmph + '</td>';
 384+ }
 385+
 386+ html += '</tr></table></div>' +
 387+ '</dd>';
 388+ } else html += '<dd class="ui_tpicker_hour" id="ui_tpicker_hour_' + dp_id + '"' +
 389+ ((o.showHour) ? '' : noDisplay) + '></dd>';
 390+
 391+ html += '<dt class="ui_tpicker_minute_label" id="ui_tpicker_minute_label_' + dp_id + '"' +
 392+ ((o.showMinute) ? '' : noDisplay) + '>' + o.minuteText + '</dt>';
 393+
 394+ // Minutes
 395+ if (o.showMinute && o.minuteGrid > 0) {
 396+ html += '<dd class="ui_tpicker_minute ui_tpicker_minute_' + o.minuteGrid + '">' +
 397+ '<div id="ui_tpicker_minute_' + dp_id + '"' +
 398+ ((o.showMinute) ? '' : noDisplay) + '></div>' +
 399+ '<div style="padding-left: 1px"><table class="ui-tpicker-grid-label"><tr>';
 400+
 401+ for (var m = o.minuteMin; m <= minMax; m += parseInt(o.minuteGrid,10)) {
 402+ minuteGridSize++;
 403+ html += '<td>' + ((m < 10) ? '0' : '') + m + '</td>';
 404+ }
 405+
 406+ html += '</tr></table></div>' +
 407+ '</dd>';
 408+ } else html += '<dd class="ui_tpicker_minute" id="ui_tpicker_minute_' + dp_id + '"' +
 409+ ((o.showMinute) ? '' : noDisplay) + '></dd>';
 410+
 411+ // Seconds
 412+ html += '<dt class="ui_tpicker_second_label" id="ui_tpicker_second_label_' + dp_id + '"' +
 413+ ((o.showSecond) ? '' : noDisplay) + '>' + o.secondText + '</dt>';
 414+
 415+ if (o.showSecond && o.secondGrid > 0) {
 416+ html += '<dd class="ui_tpicker_second ui_tpicker_second_' + o.secondGrid + '">' +
 417+ '<div id="ui_tpicker_second_' + dp_id + '"' +
 418+ ((o.showSecond) ? '' : noDisplay) + '></div>' +
 419+ '<div style="padding-left: 1px"><table><tr>';
 420+
 421+ for (var s = o.secondMin; s <= secMax; s += parseInt(o.secondGrid,10)) {
 422+ secondGridSize++;
 423+ html += '<td>' + ((s < 10) ? '0' : '') + s + '</td>';
 424+ }
 425+
 426+ html += '</tr></table></div>' +
 427+ '</dd>';
 428+ } else html += '<dd class="ui_tpicker_second" id="ui_tpicker_second_' + dp_id + '"' +
 429+ ((o.showSecond) ? '' : noDisplay) + '></dd>';
 430+
 431+ // Milliseconds
 432+ html += '<dt class="ui_tpicker_millisec_label" id="ui_tpicker_millisec_label_' + dp_id + '"' +
 433+ ((o.showMillisec) ? '' : noDisplay) + '>' + o.millisecText + '</dt>';
 434+
 435+ if (o.showMillisec && o.millisecGrid > 0) {
 436+ html += '<dd class="ui_tpicker_millisec ui_tpicker_millisec_' + o.millisecGrid + '">' +
 437+ '<div id="ui_tpicker_millisec_' + dp_id + '"' +
 438+ ((o.showMillisec) ? '' : noDisplay) + '></div>' +
 439+ '<div style="padding-left: 1px"><table><tr>';
 440+
 441+ for (var l = o.millisecMin; l <= millisecMax; l += parseInt(o.millisecGrid,10)) {
 442+ millisecGridSize++;
 443+ html += '<td>' + ((l < 10) ? '0' : '') + s + '</td>';
 444+ }
 445+
 446+ html += '</tr></table></div>' +
 447+ '</dd>';
 448+ } else html += '<dd class="ui_tpicker_millisec" id="ui_tpicker_millisec_' + dp_id + '"' +
 449+ ((o.showMillisec) ? '' : noDisplay) + '></dd>';
 450+
 451+ // Timezone
 452+ html += '<dt class="ui_tpicker_timezone_label" id="ui_tpicker_timezone_label_' + dp_id + '"' +
 453+ ((o.showTimezone) ? '' : noDisplay) + '>' + o.timezoneText + '</dt>';
 454+ html += '<dd class="ui_tpicker_timezone" id="ui_tpicker_timezone_' + dp_id + '"' +
 455+ ((o.showTimezone) ? '' : noDisplay) + '></dd>';
 456+
 457+ html += '</dl></div>';
 458+ $tp = $(html);
 459+
 460+ // if we only want time picker...
 461+ if (o.timeOnly === true) {
 462+ $tp.prepend(
 463+ '<div class="ui-widget-header ui-helper-clearfix ui-corner-all">' +
 464+ '<div class="ui-datepicker-title">' + o.timeOnlyTitle + '</div>' +
 465+ '</div>');
 466+ $dp.find('.ui-datepicker-header, .ui-datepicker-calendar').hide();
 467+ }
 468+
 469+ this.hour_slider = $tp.find('#ui_tpicker_hour_'+ dp_id).slider({
 470+ orientation: "horizontal",
 471+ value: this.hour,
 472+ min: o.hourMin,
 473+ max: hourMax,
 474+ step: o.stepHour,
 475+ slide: function(event, ui) {
 476+ tp_inst.hour_slider.slider( "option", "value", ui.value);
 477+ tp_inst._onTimeChange();
 478+ }
 479+ });
 480+
 481+ // Updated by Peter Medeiros:
 482+ // - Pass in Event and UI instance into slide function
 483+ this.minute_slider = $tp.find('#ui_tpicker_minute_'+ dp_id).slider({
 484+ orientation: "horizontal",
 485+ value: this.minute,
 486+ min: o.minuteMin,
 487+ max: minMax,
 488+ step: o.stepMinute,
 489+ slide: function(event, ui) {
 490+ // update the global minute slider instance value with the current slider value
 491+ tp_inst.minute_slider.slider( "option", "value", ui.value);
 492+ tp_inst._onTimeChange();
 493+ }
 494+ });
 495+
 496+ this.second_slider = $tp.find('#ui_tpicker_second_'+ dp_id).slider({
 497+ orientation: "horizontal",
 498+ value: this.second,
 499+ min: o.secondMin,
 500+ max: secMax,
 501+ step: o.stepSecond,
 502+ slide: function(event, ui) {
 503+ tp_inst.second_slider.slider( "option", "value", ui.value);
 504+ tp_inst._onTimeChange();
 505+ }
 506+ });
 507+
 508+ this.millisec_slider = $tp.find('#ui_tpicker_millisec_'+ dp_id).slider({
 509+ orientation: "horizontal",
 510+ value: this.millisec,
 511+ min: o.millisecMin,
 512+ max: millisecMax,
 513+ step: o.stepMillisec,
 514+ slide: function(event, ui) {
 515+ tp_inst.millisec_slider.slider( "option", "value", ui.value);
 516+ tp_inst._onTimeChange();
 517+ }
 518+ });
 519+
 520+ this.timezone_select = $tp.find('#ui_tpicker_timezone_'+ dp_id).append('<select></select>').find("select");
 521+ $.fn.append.apply(this.timezone_select,
 522+ $.map(o.timezoneList, function(val, idx) {
 523+ return $("<option />")
 524+ .val(typeof val == "object" ? val.value : val)
 525+ .text(typeof val == "object" ? val.label : val);
 526+ })
 527+ );
 528+ this.timezone_select.val((typeof this.timezone != "undefined" && this.timezone != null && this.timezone != "") ? this.timezone : o.timezone);
 529+ this.timezone_select.change(function() {
 530+ tp_inst._onTimeChange();
 531+ });
 532+
 533+ // Add grid functionality
 534+ if (o.showHour && o.hourGrid > 0) {
 535+ size = 100 * hourGridSize * o.hourGrid / (hourMax - o.hourMin);
 536+
 537+ $tp.find(".ui_tpicker_hour table").css({
 538+ width: size + "%",
 539+ marginLeft: (size / (-2 * hourGridSize)) + "%",
 540+ borderCollapse: 'collapse'
 541+ }).find("td").each( function(index) {
 542+ $(this).click(function() {
 543+ var h = $(this).html();
 544+ if(o.ampm) {
 545+ var ap = h.substring(2).toLowerCase(),
 546+ aph = parseInt(h.substring(0,2), 10);
 547+ if (ap == 'a') {
 548+ if (aph == 12) h = 0;
 549+ else h = aph;
 550+ } else if (aph == 12) h = 12;
 551+ else h = aph + 12;
 552+ }
 553+ tp_inst.hour_slider.slider("option", "value", h);
 554+ tp_inst._onTimeChange();
 555+ tp_inst._onSelectHandler();
 556+ }).css({
 557+ cursor: 'pointer',
 558+ width: (100 / hourGridSize) + '%',
 559+ textAlign: 'center',
 560+ overflow: 'hidden'
 561+ });
 562+ });
 563+ }
 564+
 565+ if (o.showMinute && o.minuteGrid > 0) {
 566+ size = 100 * minuteGridSize * o.minuteGrid / (minMax - o.minuteMin);
 567+ $tp.find(".ui_tpicker_minute table").css({
 568+ width: size + "%",
 569+ marginLeft: (size / (-2 * minuteGridSize)) + "%",
 570+ borderCollapse: 'collapse'
 571+ }).find("td").each(function(index) {
 572+ $(this).click(function() {
 573+ tp_inst.minute_slider.slider("option", "value", $(this).html());
 574+ tp_inst._onTimeChange();
 575+ tp_inst._onSelectHandler();
 576+ }).css({
 577+ cursor: 'pointer',
 578+ width: (100 / minuteGridSize) + '%',
 579+ textAlign: 'center',
 580+ overflow: 'hidden'
 581+ });
 582+ });
 583+ }
 584+
 585+ if (o.showSecond && o.secondGrid > 0) {
 586+ $tp.find(".ui_tpicker_second table").css({
 587+ width: size + "%",
 588+ marginLeft: (size / (-2 * secondGridSize)) + "%",
 589+ borderCollapse: 'collapse'
 590+ }).find("td").each(function(index) {
 591+ $(this).click(function() {
 592+ tp_inst.second_slider.slider("option", "value", $(this).html());
 593+ tp_inst._onTimeChange();
 594+ tp_inst._onSelectHandler();
 595+ }).css({
 596+ cursor: 'pointer',
 597+ width: (100 / secondGridSize) + '%',
 598+ textAlign: 'center',
 599+ overflow: 'hidden'
 600+ });
 601+ });
 602+ }
 603+
 604+ if (o.showMillisec && o.millisecGrid > 0) {
 605+ $tp.find(".ui_tpicker_millisec table").css({
 606+ width: size + "%",
 607+ marginLeft: (size / (-2 * millisecGridSize)) + "%",
 608+ borderCollapse: 'collapse'
 609+ }).find("td").each(function(index) {
 610+ $(this).click(function() {
 611+ tp_inst.millisec_slider.slider("option", "value", $(this).html());
 612+ tp_inst._onTimeChange();
 613+ tp_inst._onSelectHandler();
 614+ }).css({
 615+ cursor: 'pointer',
 616+ width: (100 / millisecGridSize) + '%',
 617+ textAlign: 'center',
 618+ overflow: 'hidden'
 619+ });
 620+ });
 621+ }
 622+
 623+ var $buttonPanel = $dp.find('.ui-datepicker-buttonpane');
 624+ if ($buttonPanel.length) $buttonPanel.before($tp);
 625+ else $dp.append($tp);
 626+
 627+ this.$timeObj = $tp.find('#ui_tpicker_time_'+ dp_id);
 628+
 629+ if (this.inst !== null) {
 630+ var timeDefined = this.timeDefined;
 631+ this._onTimeChange();
 632+ this.timeDefined = timeDefined;
 633+ }
 634+
 635+ //Emulate datepicker onSelect behavior. Call on slidestop.
 636+ var onSelectDelegate = function() {
 637+ tp_inst._onSelectHandler();
 638+ };
 639+ this.hour_slider.bind('slidestop',onSelectDelegate);
 640+ this.minute_slider.bind('slidestop',onSelectDelegate);
 641+ this.second_slider.bind('slidestop',onSelectDelegate);
 642+ this.millisec_slider.bind('slidestop',onSelectDelegate);
 643+ }
 644+ },
 645+
 646+ //########################################################################
 647+ // This function tries to limit the ability to go outside the
 648+ // min/max date range
 649+ //########################################################################
 650+ _limitMinMaxDateTime: function(dp_inst, adjustSliders){
 651+ var o = this._defaults,
 652+ dp_date = new Date(dp_inst.selectedYear, dp_inst.selectedMonth, dp_inst.selectedDay);
 653+
 654+ if(!this._defaults.showTimepicker) return; // No time so nothing to check here
 655+
 656+ if($.datepicker._get(dp_inst, 'minDateTime') !== null && $.datepicker._get(dp_inst, 'minDateTime') !== undefined && dp_date){
 657+ var minDateTime = $.datepicker._get(dp_inst, 'minDateTime'),
 658+ minDateTimeDate = new Date(minDateTime.getFullYear(), minDateTime.getMonth(), minDateTime.getDate(), 0, 0, 0, 0);
 659+
 660+ if(this.hourMinOriginal === null || this.minuteMinOriginal === null || this.secondMinOriginal === null || this.millisecMinOriginal === null){
 661+ this.hourMinOriginal = o.hourMin;
 662+ this.minuteMinOriginal = o.minuteMin;
 663+ this.secondMinOriginal = o.secondMin;
 664+ this.millisecMinOriginal = o.millisecMin;
 665+ }
 666+
 667+ if(dp_inst.settings.timeOnly || minDateTimeDate.getTime() == dp_date.getTime()) {
 668+ this._defaults.hourMin = minDateTime.getHours();
 669+ if (this.hour <= this._defaults.hourMin) {
 670+ this.hour = this._defaults.hourMin;
 671+ this._defaults.minuteMin = minDateTime.getMinutes();
 672+ if (this.minute <= this._defaults.minuteMin) {
 673+ this.minute = this._defaults.minuteMin;
 674+ this._defaults.secondMin = minDateTime.getSeconds();
 675+ } else if (this.second <= this._defaults.secondMin){
 676+ this.second = this._defaults.secondMin;
 677+ this._defaults.millisecMin = minDateTime.getMilliseconds();
 678+ } else {
 679+ if(this.millisec < this._defaults.millisecMin)
 680+ this.millisec = this._defaults.millisecMin;
 681+ this._defaults.millisecMin = this.millisecMinOriginal;
 682+ }
 683+ } else {
 684+ this._defaults.minuteMin = this.minuteMinOriginal;
 685+ this._defaults.secondMin = this.secondMinOriginal;
 686+ this._defaults.millisecMin = this.millisecMinOriginal;
 687+ }
 688+ }else{
 689+ this._defaults.hourMin = this.hourMinOriginal;
 690+ this._defaults.minuteMin = this.minuteMinOriginal;
 691+ this._defaults.secondMin = this.secondMinOriginal;
 692+ this._defaults.millisecMin = this.millisecMinOriginal;
 693+ }
 694+ }
 695+
 696+ if($.datepicker._get(dp_inst, 'maxDateTime') !== null && $.datepicker._get(dp_inst, 'maxDateTime') !== undefined && dp_date){
 697+ var maxDateTime = $.datepicker._get(dp_inst, 'maxDateTime'),
 698+ maxDateTimeDate = new Date(maxDateTime.getFullYear(), maxDateTime.getMonth(), maxDateTime.getDate(), 0, 0, 0, 0);
 699+
 700+ if(this.hourMaxOriginal === null || this.minuteMaxOriginal === null || this.secondMaxOriginal === null){
 701+ this.hourMaxOriginal = o.hourMax;
 702+ this.minuteMaxOriginal = o.minuteMax;
 703+ this.secondMaxOriginal = o.secondMax;
 704+ this.millisecMaxOriginal = o.millisecMax;
 705+ }
 706+
 707+ if(dp_inst.settings.timeOnly || maxDateTimeDate.getTime() == dp_date.getTime()){
 708+ this._defaults.hourMax = maxDateTime.getHours();
 709+ if (this.hour >= this._defaults.hourMax) {
 710+ this.hour = this._defaults.hourMax;
 711+ this._defaults.minuteMax = maxDateTime.getMinutes();
 712+ if (this.minute >= this._defaults.minuteMax) {
 713+ this.minute = this._defaults.minuteMax;
 714+ this._defaults.secondMax = maxDateTime.getSeconds();
 715+ } else if (this.second >= this._defaults.secondMax) {
 716+ this.second = this._defaults.secondMax;
 717+ this._defaults.millisecMax = maxDateTime.getMilliseconds();
 718+ } else {
 719+ if(this.millisec > this._defaults.millisecMax) this.millisec = this._defaults.millisecMax;
 720+ this._defaults.millisecMax = this.millisecMaxOriginal;
 721+ }
 722+ } else {
 723+ this._defaults.minuteMax = this.minuteMaxOriginal;
 724+ this._defaults.secondMax = this.secondMaxOriginal;
 725+ this._defaults.millisecMax = this.millisecMaxOriginal;
 726+ }
 727+ }else{
 728+ this._defaults.hourMax = this.hourMaxOriginal;
 729+ this._defaults.minuteMax = this.minuteMaxOriginal;
 730+ this._defaults.secondMax = this.secondMaxOriginal;
 731+ this._defaults.millisecMax = this.millisecMaxOriginal;
 732+ }
 733+ }
 734+
 735+ if(adjustSliders !== undefined && adjustSliders === true){
 736+ var hourMax = (this._defaults.hourMax - ((this._defaults.hourMax - this._defaults.hourMin) % this._defaults.stepHour)).toFixed(0),
 737+ minMax = (this._defaults.minuteMax - ((this._defaults.minuteMax - this._defaults.minuteMin) % this._defaults.stepMinute)).toFixed(0),
 738+ secMax = (this._defaults.secondMax - ((this._defaults.secondMax - this._defaults.secondMin) % this._defaults.stepSecond)).toFixed(0),
 739+ millisecMax = (this._defaults.millisecMax - ((this._defaults.millisecMax - this._defaults.millisecMin) % this._defaults.stepMillisec)).toFixed(0);
 740+
 741+ if(this.hour_slider)
 742+ this.hour_slider.slider("option", { min: this._defaults.hourMin, max: hourMax }).slider('value', this.hour);
 743+ if(this.minute_slider)
 744+ this.minute_slider.slider("option", { min: this._defaults.minuteMin, max: minMax }).slider('value', this.minute);
 745+ if(this.second_slider)
 746+ this.second_slider.slider("option", { min: this._defaults.secondMin, max: secMax }).slider('value', this.second);
 747+ if(this.millisec_slider)
 748+ this.millisec_slider.slider("option", { min: this._defaults.millisecMin, max: millisecMax }).slider('value', this.millisec);
 749+ }
 750+
 751+ },
 752+
 753+
 754+ //########################################################################
 755+ // when a slider moves, set the internal time...
 756+ // on time change is also called when the time is updated in the text field
 757+ //########################################################################
 758+ _onTimeChange: function() {
 759+ var hour = (this.hour_slider) ? this.hour_slider.slider('value') : false,
 760+ minute = (this.minute_slider) ? this.minute_slider.slider('value') : false,
 761+ second = (this.second_slider) ? this.second_slider.slider('value') : false,
 762+ millisec = (this.millisec_slider) ? this.millisec_slider.slider('value') : false,
 763+ timezone = (this.timezone_select) ? this.timezone_select.val() : false,
 764+ o = this._defaults;
 765+
 766+ if (typeof(hour) == 'object') hour = false;
 767+ if (typeof(minute) == 'object') minute = false;
 768+ if (typeof(second) == 'object') second = false;
 769+ if (typeof(millisec) == 'object') millisec = false;
 770+ if (typeof(timezone) == 'object') timezone = false;
 771+
 772+ if (hour !== false) hour = parseInt(hour,10);
 773+ if (minute !== false) minute = parseInt(minute,10);
 774+ if (second !== false) second = parseInt(second,10);
 775+ if (millisec !== false) millisec = parseInt(millisec,10);
 776+
 777+ var ampm = o[hour < 12 ? 'amNames' : 'pmNames'][0];
 778+
 779+ // If the update was done in the input field, the input field should not be updated.
 780+ // If the update was done using the sliders, update the input field.
 781+ var hasChanged = (hour != this.hour || minute != this.minute
 782+ || second != this.second || millisec != this.millisec
 783+ || (this.ampm.length > 0
 784+ && (hour < 12) != ($.inArray(this.ampm.toUpperCase(), this.amNames) !== -1))
 785+ || timezone != this.timezone);
 786+
 787+ if (hasChanged) {
 788+
 789+ if (hour !== false)this.hour = hour;
 790+ if (minute !== false) this.minute = minute;
 791+ if (second !== false) this.second = second;
 792+ if (millisec !== false) this.millisec = millisec;
 793+ if (timezone !== false) this.timezone = timezone;
 794+
 795+ if (!this.inst) this.inst = $.datepicker._getInst(this.$input[0]);
 796+
 797+ this._limitMinMaxDateTime(this.inst, true);
 798+ }
 799+ if (o.ampm) this.ampm = ampm;
 800+
 801+ this._formatTime();
 802+ if (this.$timeObj) this.$timeObj.text(this.formattedTime + o.timeSuffix);
 803+ this.timeDefined = true;
 804+ if (hasChanged) this._updateDateTime();
 805+ },
 806+
 807+ //########################################################################
 808+ // call custom onSelect.
 809+ // bind to sliders slidestop, and grid click.
 810+ //########################################################################
 811+ _onSelectHandler: function() {
 812+ var onSelect = this._defaults.onSelect;
 813+ var inputEl = this.$input ? this.$input[0] : null;
 814+ if (onSelect && inputEl) {
 815+ onSelect.apply(inputEl, [this.formattedDateTime, this]);
 816+ }
 817+ },
 818+
 819+ //########################################################################
 820+ // format the time all pretty...
 821+ //########################################################################
 822+ _formatTime: function(time, format, ampm) {
 823+ if (ampm == undefined) ampm = this._defaults.ampm;
 824+ time = time || { hour: this.hour, minute: this.minute, second: this.second, millisec: this.millisec, ampm: this.ampm, timezone: this.timezone };
 825+ var tmptime = (format || this._defaults.timeFormat).toString();
 826+
 827+ var hour = parseInt(time.hour, 10);
 828+ if (ampm) {
 829+ if (!$.inArray(time.ampm.toUpperCase(), this.amNames) !== -1)
 830+ hour = hour % 12;
 831+ if (hour === 0)
 832+ hour = 12;
 833+ }
 834+ tmptime = tmptime.replace(/(?:hh?|mm?|ss?|[tT]{1,2}|[lz])/g, function(match) {
 835+ switch (match.toLowerCase()) {
 836+ case 'hh': return ('0' + hour).slice(-2);
 837+ case 'h': return hour;
 838+ case 'mm': return ('0' + time.minute).slice(-2);
 839+ case 'm': return time.minute;
 840+ case 'ss': return ('0' + time.second).slice(-2);
 841+ case 's': return time.second;
 842+ case 'l': return ('00' + time.millisec).slice(-3);
 843+ case 'z': return time.timezone;
 844+ case 't': case 'tt':
 845+ if (ampm) {
 846+ var _ampm = time.ampm;
 847+ if (match.length == 1)
 848+ _ampm = _ampm.charAt(0);
 849+ return match.charAt(0) == 'T' ? _ampm.toUpperCase() : _ampm.toLowerCase();
 850+ }
 851+ return '';
 852+ }
 853+ });
 854+
 855+ if (arguments.length) return tmptime;
 856+ else this.formattedTime = tmptime;
 857+ },
 858+
 859+ //########################################################################
 860+ // update our input with the new date time..
 861+ //########################################################################
 862+ _updateDateTime: function(dp_inst) {
 863+ dp_inst = this.inst || dp_inst,
 864+ dt = new Date(dp_inst.selectedYear, dp_inst.selectedMonth, dp_inst.selectedDay),
 865+ dateFmt = $.datepicker._get(dp_inst, 'dateFormat'),
 866+ formatCfg = $.datepicker._getFormatConfig(dp_inst),
 867+ timeAvailable = dt !== null && this.timeDefined;
 868+ this.formattedDate = $.datepicker.formatDate(dateFmt, (dt === null ? new Date() : dt), formatCfg);
 869+ var formattedDateTime = this.formattedDate;
 870+ if (dp_inst.lastVal !== undefined && (dp_inst.lastVal.length > 0 && this.$input.val().length === 0))
 871+ return;
 872+
 873+ if (this._defaults.timeOnly === true) {
 874+ formattedDateTime = this.formattedTime;
 875+ } else if (this._defaults.timeOnly !== true && (this._defaults.alwaysSetTime || timeAvailable)) {
 876+ formattedDateTime += this._defaults.separator + this.formattedTime + this._defaults.timeSuffix;
 877+ }
 878+
 879+ this.formattedDateTime = formattedDateTime;
 880+
 881+ if(!this._defaults.showTimepicker) {
 882+ this.$input.val(this.formattedDate);
 883+ } else if (this.$altInput && this._defaults.altFieldTimeOnly === true) {
 884+ this.$altInput.val(this.formattedTime);
 885+ this.$input.val(this.formattedDate);
 886+ } else if(this.$altInput) {
 887+ this.$altInput.val(formattedDateTime);
 888+ this.$input.val(formattedDateTime);
 889+ } else {
 890+ this.$input.val(formattedDateTime);
 891+ }
 892+
 893+ this.$input.trigger("change");
 894+ }
 895+
 896+});
 897+
 898+$.fn.extend({
 899+ //########################################################################
 900+ // shorthand just to use timepicker..
 901+ //########################################################################
 902+ timepicker: function(o) {
 903+ o = o || {};
 904+ var tmp_args = arguments;
 905+
 906+ if (typeof o == 'object') tmp_args[0] = $.extend(o, { timeOnly: true });
 907+
 908+ return $(this).each(function() {
 909+ $.fn.datetimepicker.apply($(this), tmp_args);
 910+ });
 911+ },
 912+
 913+ //########################################################################
 914+ // extend timepicker to datepicker
 915+ //########################################################################
 916+ datetimepicker: function(o) {
 917+ o = o || {};
 918+ var $input = this,
 919+ tmp_args = arguments;
 920+
 921+ if (typeof(o) == 'string'){
 922+ if(o == 'getDate')
 923+ return $.fn.datepicker.apply($(this[0]), tmp_args);
 924+ else
 925+ return this.each(function() {
 926+ var $t = $(this);
 927+ $t.datepicker.apply($t, tmp_args);
 928+ });
 929+ }
 930+ else
 931+ return this.each(function() {
 932+ var $t = $(this);
 933+ $t.datepicker($.timepicker._newInst($t, o)._defaults);
 934+ });
 935+ }
 936+});
 937+
 938+//########################################################################
 939+// the bad hack :/ override datepicker so it doesnt close on select
 940+// inspired: http://stackoverflow.com/questions/1252512/jquery-datepicker-prevent-closing-picker-when-clicking-a-date/1762378#1762378
 941+//########################################################################
 942+$.datepicker._base_selectDate = $.datepicker._selectDate;
 943+$.datepicker._selectDate = function (id, dateStr) {
 944+ var inst = this._getInst($(id)[0]),
 945+ tp_inst = this._get(inst, 'timepicker');
 946+
 947+ if (tp_inst) {
 948+ tp_inst._limitMinMaxDateTime(inst, true);
 949+ inst.inline = inst.stay_open = true;
 950+ //This way the onSelect handler called from calendarpicker get the full dateTime
 951+ this._base_selectDate(id, dateStr);
 952+ inst.inline = inst.stay_open = false;
 953+ this._notifyChange(inst);
 954+ this._updateDatepicker(inst);
 955+ }
 956+ else this._base_selectDate(id, dateStr);
 957+};
 958+
 959+//#############################################################################################
 960+// second bad hack :/ override datepicker so it triggers an event when changing the input field
 961+// and does not redraw the datepicker on every selectDate event
 962+//#############################################################################################
 963+$.datepicker._base_updateDatepicker = $.datepicker._updateDatepicker;
 964+$.datepicker._updateDatepicker = function(inst) {
 965+
 966+ // don't popup the datepicker if there is another instance already opened
 967+ var input = inst.input[0];
 968+ if($.datepicker._curInst &&
 969+ $.datepicker._curInst != inst &&
 970+ $.datepicker._datepickerShowing &&
 971+ $.datepicker._lastInput != input) {
 972+ return;
 973+ }
 974+
 975+ if (typeof(inst.stay_open) !== 'boolean' || inst.stay_open === false) {
 976+
 977+ this._base_updateDatepicker(inst);
 978+
 979+ // Reload the time control when changing something in the input text field.
 980+ var tp_inst = this._get(inst, 'timepicker');
 981+ if(tp_inst) tp_inst._addTimePicker(inst);
 982+ }
 983+};
 984+
 985+//#######################################################################################
 986+// third bad hack :/ override datepicker so it allows spaces and colon in the input field
 987+//#######################################################################################
 988+$.datepicker._base_doKeyPress = $.datepicker._doKeyPress;
 989+$.datepicker._doKeyPress = function(event) {
 990+ var inst = $.datepicker._getInst(event.target),
 991+ tp_inst = $.datepicker._get(inst, 'timepicker');
 992+
 993+ if (tp_inst) {
 994+ if ($.datepicker._get(inst, 'constrainInput')) {
 995+ var ampm = tp_inst._defaults.ampm,
 996+ dateChars = $.datepicker._possibleChars($.datepicker._get(inst, 'dateFormat')),
 997+ datetimeChars = tp_inst._defaults.timeFormat.toString()
 998+ .replace(/[hms]/g, '')
 999+ .replace(/TT/g, ampm ? 'APM' : '')
 1000+ .replace(/Tt/g, ampm ? 'AaPpMm' : '')
 1001+ .replace(/tT/g, ampm ? 'AaPpMm' : '')
 1002+ .replace(/T/g, ampm ? 'AP' : '')
 1003+ .replace(/tt/g, ampm ? 'apm' : '')
 1004+ .replace(/t/g, ampm ? 'ap' : '') +
 1005+ " " +
 1006+ tp_inst._defaults.separator +
 1007+ tp_inst._defaults.timeSuffix +
 1008+ (tp_inst._defaults.showTimezone ? tp_inst._defaults.timezoneList.join('') : '') +
 1009+ (tp_inst._defaults.amNames.join('')) +
 1010+ (tp_inst._defaults.pmNames.join('')) +
 1011+ dateChars,
 1012+ chr = String.fromCharCode(event.charCode === undefined ? event.keyCode : event.charCode);
 1013+ return event.ctrlKey || (chr < ' ' || !dateChars || datetimeChars.indexOf(chr) > -1);
 1014+ }
 1015+ }
 1016+
 1017+ return $.datepicker._base_doKeyPress(event);
 1018+};
 1019+
 1020+//#######################################################################################
 1021+// Override key up event to sync manual input changes.
 1022+//#######################################################################################
 1023+$.datepicker._base_doKeyUp = $.datepicker._doKeyUp;
 1024+$.datepicker._doKeyUp = function (event) {
 1025+ var inst = $.datepicker._getInst(event.target),
 1026+ tp_inst = $.datepicker._get(inst, 'timepicker');
 1027+
 1028+ if (tp_inst) {
 1029+ if (tp_inst._defaults.timeOnly && (inst.input.val() != inst.lastVal)) {
 1030+ try {
 1031+ $.datepicker._updateDatepicker(inst);
 1032+ }
 1033+ catch (err) {
 1034+ $.datepicker.log(err);
 1035+ }
 1036+ }
 1037+ }
 1038+
 1039+ return $.datepicker._base_doKeyUp(event);
 1040+};
 1041+
 1042+//#######################################################################################
 1043+// override "Today" button to also grab the time.
 1044+//#######################################################################################
 1045+$.datepicker._base_gotoToday = $.datepicker._gotoToday;
 1046+$.datepicker._gotoToday = function(id) {
 1047+ var inst = this._getInst($(id)[0]),
 1048+ $dp = inst.dpDiv;
 1049+ this._base_gotoToday(id);
 1050+ var now = new Date();
 1051+ var tp_inst = this._get(inst, 'timepicker');
 1052+ if (tp_inst._defaults.showTimezone && tp_inst.timezone_select) {
 1053+ var tzoffset = now.getTimezoneOffset(); // If +0100, returns -60
 1054+ var tzsign = tzoffset > 0 ? '-' : '+';
 1055+ tzoffset = Math.abs(tzoffset);
 1056+ var tzmin = tzoffset % 60
 1057+ tzoffset = tzsign + ('0' + (tzoffset - tzmin) / 60).slice(-2) + ('0' + tzmin).slice(-2);
 1058+ if (tp_inst._defaults.timezoneIso8609)
 1059+ tzoffset = tzoffset.substring(0, 3) + ':' + tzoffset.substring(3);
 1060+ tp_inst.timezone_select.val(tzoffset);
 1061+ }
 1062+ this._setTime(inst, now);
 1063+ $( '.ui-datepicker-today', $dp).click();
 1064+};
 1065+
 1066+//#######################################################################################
 1067+// Disable & enable the Time in the datetimepicker
 1068+//#######################################################################################
 1069+$.datepicker._disableTimepickerDatepicker = function(target, date, withDate) {
 1070+ var inst = this._getInst(target),
 1071+ tp_inst = this._get(inst, 'timepicker');
 1072+ $(target).datepicker('getDate'); // Init selected[Year|Month|Day]
 1073+ if (tp_inst) {
 1074+ tp_inst._defaults.showTimepicker = false;
 1075+ tp_inst._updateDateTime(inst);
 1076+ }
 1077+};
 1078+
 1079+$.datepicker._enableTimepickerDatepicker = function(target, date, withDate) {
 1080+ var inst = this._getInst(target),
 1081+ tp_inst = this._get(inst, 'timepicker');
 1082+ $(target).datepicker('getDate'); // Init selected[Year|Month|Day]
 1083+ if (tp_inst) {
 1084+ tp_inst._defaults.showTimepicker = true;
 1085+ tp_inst._addTimePicker(inst); // Could be disabled on page load
 1086+ tp_inst._updateDateTime(inst);
 1087+ }
 1088+};
 1089+
 1090+//#######################################################################################
 1091+// Create our own set time function
 1092+//#######################################################################################
 1093+$.datepicker._setTime = function(inst, date) {
 1094+ var tp_inst = this._get(inst, 'timepicker');
 1095+ if (tp_inst) {
 1096+ var defaults = tp_inst._defaults,
 1097+ // calling _setTime with no date sets time to defaults
 1098+ hour = date ? date.getHours() : defaults.hour,
 1099+ minute = date ? date.getMinutes() : defaults.minute,
 1100+ second = date ? date.getSeconds() : defaults.second,
 1101+ millisec = date ? date.getMilliseconds() : defaults.millisec;
 1102+
 1103+ //check if within min/max times..
 1104+ if ((hour < defaults.hourMin || hour > defaults.hourMax) || (minute < defaults.minuteMin || minute > defaults.minuteMax) || (second < defaults.secondMin || second > defaults.secondMax) || (millisec < defaults.millisecMin || millisec > defaults.millisecMax)) {
 1105+ hour = defaults.hourMin;
 1106+ minute = defaults.minuteMin;
 1107+ second = defaults.secondMin;
 1108+ millisec = defaults.millisecMin;
 1109+ }
 1110+
 1111+ tp_inst.hour = hour;
 1112+ tp_inst.minute = minute;
 1113+ tp_inst.second = second;
 1114+ tp_inst.millisec = millisec;
 1115+
 1116+ if (tp_inst.hour_slider) tp_inst.hour_slider.slider('value', hour);
 1117+ if (tp_inst.minute_slider) tp_inst.minute_slider.slider('value', minute);
 1118+ if (tp_inst.second_slider) tp_inst.second_slider.slider('value', second);
 1119+ if (tp_inst.millisec_slider) tp_inst.millisec_slider.slider('value', millisec);
 1120+
 1121+ tp_inst._onTimeChange();
 1122+ tp_inst._updateDateTime(inst);
 1123+ }
 1124+};
 1125+
 1126+//#######################################################################################
 1127+// Create new public method to set only time, callable as $().datepicker('setTime', date)
 1128+//#######################################################################################
 1129+$.datepicker._setTimeDatepicker = function(target, date, withDate) {
 1130+ var inst = this._getInst(target),
 1131+ tp_inst = this._get(inst, 'timepicker');
 1132+
 1133+ if (tp_inst) {
 1134+ this._setDateFromField(inst);
 1135+ var tp_date;
 1136+ if (date) {
 1137+ if (typeof date == "string") {
 1138+ tp_inst._parseTime(date, withDate);
 1139+ tp_date = new Date();
 1140+ tp_date.setHours(tp_inst.hour, tp_inst.minute, tp_inst.second, tp_inst.millisec);
 1141+ }
 1142+ else tp_date = new Date(date.getTime());
 1143+ if (tp_date.toString() == 'Invalid Date') tp_date = undefined;
 1144+ this._setTime(inst, tp_date);
 1145+ }
 1146+ }
 1147+
 1148+};
 1149+
 1150+//#######################################################################################
 1151+// override setDate() to allow setting time too within Date object
 1152+//#######################################################################################
 1153+$.datepicker._base_setDateDatepicker = $.datepicker._setDateDatepicker;
 1154+$.datepicker._setDateDatepicker = function(target, date) {
 1155+ var inst = this._getInst(target),
 1156+ tp_date = (date instanceof Date) ? new Date(date.getTime()) : date;
 1157+
 1158+ this._updateDatepicker(inst);
 1159+ this._base_setDateDatepicker.apply(this, arguments);
 1160+ this._setTimeDatepicker(target, tp_date, true);
 1161+};
 1162+
 1163+//#######################################################################################
 1164+// override getDate() to allow getting time too within Date object
 1165+//#######################################################################################
 1166+$.datepicker._base_getDateDatepicker = $.datepicker._getDateDatepicker;
 1167+$.datepicker._getDateDatepicker = function(target, noDefault) {
 1168+ var inst = this._getInst(target),
 1169+ tp_inst = this._get(inst, 'timepicker');
 1170+
 1171+ if (tp_inst) {
 1172+ this._setDateFromField(inst, noDefault);
 1173+ var date = this._getDate(inst);
 1174+ if (date && tp_inst._parseTime($(target).val(), tp_inst.timeOnly)) date.setHours(tp_inst.hour, tp_inst.minute, tp_inst.second, tp_inst.millisec);
 1175+ return date;
 1176+ }
 1177+ return this._base_getDateDatepicker(target, noDefault);
 1178+};
 1179+
 1180+//#######################################################################################
 1181+// override parseDate() because UI 1.8.14 throws an error about "Extra characters"
 1182+// An option in datapicker to ignore extra format characters would be nicer.
 1183+//#######################################################################################
 1184+$.datepicker._base_parseDate = $.datepicker.parseDate;
 1185+$.datepicker.parseDate = function(format, value, settings) {
 1186+ var date;
 1187+ try {
 1188+ date = this._base_parseDate(format, value, settings);
 1189+ } catch (err) {
 1190+ // Hack! The error message ends with a colon, a space, and
 1191+ // the "extra" characters. We rely on that instead of
 1192+ // attempting to perfectly reproduce the parsing algorithm.
 1193+ date = this._base_parseDate(format, value.substring(0,value.length-(err.length-err.indexOf(':')-2)), settings);
 1194+ }
 1195+ return date;
 1196+};
 1197+
 1198+//#######################################################################################
 1199+// override formatDate to set date with time to the input
 1200+//#######################################################################################
 1201+$.datepicker._base_formatDate=$.datepicker._formatDate;
 1202+$.datepicker._formatDate = function(inst, day, month, year){
 1203+ var tp_inst = this._get(inst, 'timepicker');
 1204+ if(tp_inst)
 1205+ {
 1206+ if(day)
 1207+ var b = this._base_formatDate(inst, day, month, year);
 1208+ tp_inst._updateDateTime();
 1209+ return tp_inst.$input.val();
 1210+ }
 1211+ return this._base_formatDate(inst);
 1212+}
 1213+
 1214+//#######################################################################################
 1215+// override options setter to add time to maxDate(Time) and minDate(Time). MaxDate
 1216+//#######################################################################################
 1217+$.datepicker._base_optionDatepicker = $.datepicker._optionDatepicker;
 1218+$.datepicker._optionDatepicker = function(target, name, value) {
 1219+ var inst = this._getInst(target),
 1220+ tp_inst = this._get(inst, 'timepicker');
 1221+ if (tp_inst) {
 1222+ var min,max,onselect;
 1223+ if (typeof name == 'string') { // if min/max was set with the string
 1224+ if (name==='minDate' || name==='minDateTime' )
 1225+ min = value;
 1226+ else if (name==='maxDate' || name==='maxDateTime')
 1227+ max = value;
 1228+ else if (name==='onSelect')
 1229+ onselect=value;
 1230+ } else if (typeof name == 'object') { //if min/max was set with the JSON
 1231+ if(name.minDate)
 1232+ min = name.minDate;
 1233+ else if (name.minDateTime)
 1234+ min = name.minDateTime;
 1235+ else if (name.maxDate)
 1236+ max = name.maxDate;
 1237+ else if (name.maxDateTime)
 1238+ max = name.maxDateTime;
 1239+ }
 1240+ if(min){ //if min was set
 1241+ if(min==0)
 1242+ min=new Date();
 1243+ else
 1244+ min= new Date(min);
 1245+
 1246+ tp_inst._defaults.minDate = min;
 1247+ tp_inst._defaults.minDateTime = min;
 1248+ } else if (max){ //if max was set
 1249+ if(max==0)
 1250+ max=new Date();
 1251+ else
 1252+ max= new Date(max);
 1253+ tp_inst._defaults.maxDate = max;
 1254+ tp_inst._defaults.maxDateTime = max;
 1255+ }
 1256+ else if (onselect)
 1257+ tp_inst._defaults.onSelect=onselect;
 1258+ }
 1259+ this._base_optionDatepicker(target, name, value);
 1260+};
 1261+
 1262+//#######################################################################################
 1263+// jQuery extend now ignores nulls!
 1264+//#######################################################################################
 1265+function extendRemove(target, props) {
 1266+ $.extend(target, props);
 1267+ for (var name in props)
 1268+ if (props[name] === null || props[name] === undefined)
 1269+ target[name] = props[name];
 1270+ return target;
 1271+}
 1272+
 1273+$.timepicker = new Timepicker(); // singleton instance
 1274+$.timepicker.version = "0.9.7";
 1275+
 1276+})(jQuery);
 1277+
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/resources/jquery.ui.timepicker.js
___________________________________________________________________
Added: svn:eol-style
11278 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/Contest.sql
@@ -0,0 +1,96 @@
 2+-- MySQL version of the database schema for the Contest extension.
 3+-- Licence: GNU GPL v3+
 4+-- Author: Jeroen De Dauw < jeroendedauw@gmail.com >
 5+
 6+-- Contests
 7+CREATE TABLE IF NOT EXISTS /*_*/contests (
 8+ contest_id SMALLINT unsigned NOT NULL auto_increment PRIMARY KEY,
 9+ contest_name VARCHAR(255) NOT NULL, -- String indentifier for the contest
 10+ contest_status TINYINT unsigned NOT NULL default '0', -- Status of the contest. One of 0 (DRAFT), 1 (ACTIVE), 2 (FINISHED)
 11+ contest_end varbinary(14) NOT NULL default '', -- End time of the contest (MW timestamp)
 12+
 13+ contest_rules_page VARCHAR(255) NOT NULL, -- Name of the page with rules
 14+ contest_opportunities VARCHAR(255) NOT NULL, -- Name of the page with opportunities
 15+ contest_intro VARCHAR(255) NOT NULL, -- Name of the page with the intro text
 16+ contest_help VARCHAR(255) NOT NULL, -- Name of the page with contest help
 17+ contest_signup_email VARCHAR(255) NOT NULL, -- Name of the page with the signup email text
 18+ contest_reminder_email VARCHAR(255) NOT NULL, -- Name of the page with the reminder email text
 19+
 20+ contest_submission_count SMALLINT unsigned NOT NULL -- Number of submissions made to the contest
 21+) /*$wgDBTableOptions*/;
 22+CREATE UNIQUE INDEX /*i*/contests_name ON /*_*/contests (contest_name);
 23+CREATE INDEX /*i*/contests_status_end ON /*_*/contests (contest_status, contest_end);
 24+
 25+-- Contestants
 26+CREATE TABLE IF NOT EXISTS /*_*/contest_contestants (
 27+ contestant_id INT unsigned NOT NULL auto_increment PRIMARY KEY, -- Contestant id (unique id per user per contest)
 28+ contestant_contest_id SMALLINT unsigned NOT NULL, -- Foreign key on contests.contest_id
 29+ contestant_user_id INT(10) unsigned NOT NULL, -- Foreign key on user.user_id
 30+ contestant_challenge_id INT unsigned NOT NULL, -- Foreign key on contest_challenges.challenge_id
 31+
 32+ -- These fields will be copied from the user table on contest lock
 33+ contestant_full_name VARCHAR(255) NOT NULL, -- Full name of the contestant
 34+ contestant_user_name VARCHAR(255) NOT NULL, -- User name of the contestant
 35+ contestant_email TINYBLOB NOT NULL, -- Email of the contestant
 36+
 37+ -- Extra contestant info
 38+ contestant_country VARCHAR(255) NOT NULL, -- Country code of the contestant
 39+ contestant_volunteer TINYINT unsigned NOT NULL, -- If the user is interested in volunteer opportunities
 40+ contestant_wmf TINYINT unsigned NOT NULL, -- If the user is interested in a WMF job
 41+ contestant_cv TINYBLOB NOT NULL, -- URL to the users CV
 42+
 43+ contestant_submission TINYBLOB NOT NULL, -- URL to the users submission
 44+
 45+ contestant_rating TINYINT unsigned NOT NULL, -- The average rating of the contestant
 46+ contestant_rating_count SMALLINT unsigned NOT NULL, -- The number of ratings
 47+ contestant_comments SMALLINT unsigned NOT NULL -- The number of comments
 48+) /*$wgDBTableOptions*/;
 49+-- TODO: probably need to split indexes, see queries in ContestantPager
 50+CREATE INDEX /*i*/contest_contestants_contest_id ON /*_*/contest_contestants (contestant_contest_id, contestant_id);
 51+CREATE INDEX /*i*/contest_contestants_challenge_id ON /*_*/contest_contestants (contestant_challenge_id, contestant_id);
 52+CREATE INDEX /*i*/contest_contestants_contest_challenge ON /*_*/contest_contestants (contestant_contest_id, contestant_challenge_id);
 53+CREATE INDEX /*i*/contest_contestants_contest_volunteer ON /*_*/contest_contestants (contestant_contest_id, contestant_volunteer);
 54+CREATE INDEX /*i*/contest_contestants_challenge_volunteer ON /*_*/contest_contestants (contestant_challenge_id, contestant_volunteer);
 55+CREATE INDEX /*i*/contest_contestants_contest_wmf ON /*_*/contest_contestants (contestant_contest_id, contestant_wmf);
 56+CREATE INDEX /*i*/contest_contestants_challenge_wmf ON /*_*/contest_contestants (contestant_challenge_id, contestant_wmf);
 57+CREATE INDEX /*i*/contest_contestants_contest_comments ON /*_*/contest_contestants (contestant_contest_id, contestant_comments);
 58+CREATE INDEX /*i*/contest_contestants_challenge_comments ON /*_*/contest_contestants (contestant_challenge_id, contestant_comments);
 59+CREATE INDEX /*i*/contest_contestants_contest_rating ON /*_*/contest_contestants (contestant_contest_id, contestant_rating);
 60+CREATE INDEX /*i*/contest_contestants_challenge_rating ON /*_*/contest_contestants (contestant_challenge_id, contestant_rating);
 61+CREATE INDEX /*i*/contest_contestants_contest_rating_count ON /*_*/contest_contestants (contestant_contest_id, contestant_rating_count);
 62+CREATE INDEX /*i*/contest_contestants_challenge_rating_count ON /*_*/contest_contestants (contestant_challenge_id, contestant_rating_count);
 63+CREATE UNIQUE INDEX /*i*/contest_contestants_id_user ON /*_*/contest_contestants (contestant_contest_id, contestant_user_id);
 64+
 65+-- Challenges
 66+CREATE TABLE IF NOT EXISTS /*_*/contest_challenges (
 67+ challenge_id INT unsigned NOT NULL auto_increment PRIMARY KEY, -- Challenge id
 68+ challenge_contest_id INT unsigned NOT NULL, -- Foreign key on contests.contest_id
 69+
 70+ challenge_text TEXT NOT NULL, -- Full challenge description
 71+ challenge_title VARCHAR(255) NOT NULL, -- Title of the challenge
 72+ challenge_oneline TEXT NOT NULL -- One line description of the challenge
 73+) /*$wgDBTableOptions*/;
 74+CREATE INDEX /*i*/contest_challenges_contest_id ON /*_*/contest_challenges (challenge_contest_id);
 75+CREATE UNIQUE INDEX /*i*/contest_challenges_title ON /*_*/contest_challenges (challenge_title);
 76+
 77+-- Judge votes
 78+CREATE TABLE IF NOT EXISTS /*_*/contest_votes (
 79+ vote_id INT unsigned NOT NULL auto_increment PRIMARY KEY,
 80+ vote_contestant_id INT unsigned NOT NULL, -- Foreign key on contest_contestants.contestant_id
 81+ vote_user_id INT(10) unsigned NOT NULL, -- Judge user id
 82+
 83+ vote_value SMALLINT NOT NULL -- The value of the vote
 84+) /*$wgDBTableOptions*/;
 85+CREATE UNIQUE INDEX /*i*/contest_votes_contestant_user ON /*_*/contest_votes (vote_contestant_id, vote_user_id);
 86+CREATE INDEX /*i*/contest_votes_user ON /*_*/contest_votes (vote_user_id);
 87+
 88+-- Judge comments
 89+CREATE TABLE IF NOT EXISTS /*_*/contest_comments (
 90+ comment_id INT unsigned NOT NULL auto_increment PRIMARY KEY,
 91+ comment_contestant_id INT unsigned NOT NULL, -- Foreign key on contest_contestants.contestant_id
 92+ comment_user_id INT(10) unsigned NOT NULL, -- Judge user id
 93+
 94+ comment_text TEXT NOT NULL, -- The comment text
 95+ comment_time varbinary(14) NOT NULL default '' -- The time at which the comment was made
 96+) /*$wgDBTableOptions*/;
 97+CREATE INDEX /*i*/contest_comments_id_time ON /*_*/contest_comments (comment_contestant_id, comment_time);
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/Contest.sql
___________________________________________________________________
Added: svn:eol-style
198 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/test/ContestValidationTests.php
@@ -0,0 +1,57 @@
 2+<?php
 3+
 4+/**
 5+ * Contest form field validation tests cases.
 6+ *
 7+ * @ingroup Contest
 8+ * @since 0.1
 9+ *
 10+ * @licence GNU GPL v3
 11+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
 12+ */
 13+class ContestValidationTests extends MediaWikiTestCase {
 14+
 15+ /**
 16+ * Tests @see SpecialMyContests::::validateSubmissionField
 17+ */
 18+ public function testURLValidation() {
 19+ $tests = array(
 20+ 'https://github.com/JeroenDeDauw/smwcon/tree/f9b26ec4ba1101b1f5d4ef76b7ae6ad3dabfb53b' => true,
 21+ 'https://github.com/Jeroen-De-Dauw42/smwcon_-42/tree/f9b26ec4ba1101b1f5d4ef76b7ae6ad3dabfb53b' => true,
 22+// 'https://github.com/JeroenDeDauw$/smwcon/tree/f9b26ec4ba1101b1f5d4ef76b7ae6ad3dabfb53b' => false,
 23+// 'https://github.com/JeroenDeDauw/smwcon/tree3/f9b26ec4ba1101b1f5d4ef76b7ae6ad3dabfb53b' => false,
 24+// 'https://github.com/JeroenDeDauw/smwcon/tree/f9b26ec4ba1101b1f5d4ef76b7ae6ad3dabfb53' => false,
 25+// 'https://github.com/JeroenDeDauw/smwcon/tree/f9b26ec4ba1101b1f5d4ef76b7ae6ad3dabfb53ba' => false,
 26+// 'https://github.com/JeroenDeDauw/smwc*/tree/f9b26ec4ba1101b1f5d4ef76b7ae6ad3dabfb53b' => false,
 27+ 'in ur unit test, being quite silly' => false,
 28+ ' https://github.com/JeroenDeDauw/smwcon/tree/f9b26ec4ba1101b1f5d4ef76b7ae6ad3dabfb53b ' => true,
 29+ '' => true,
 30+ ' ' => true,
 31+ 'https://github.com/JeroenDeDauw/smwcon' => true,
 32+ 'https://github.com/JeroenDeDauw' => true,
 33+ 'https://gitorious.org/statusnet' => true,
 34+ 'https://gitorious.org/statusnet/mainline/merge_requests/2224' => true,
 35+ );
 36+
 37+ foreach ( $tests as $test => $isValdid ) {
 38+ if ( $isValdid ) {
 39+ $this->assertEquals( true, SpecialMyContests::validateSubmissionField( $test ) );
 40+ }
 41+ else {
 42+ $this->assertFalse( SpecialMyContests::validateSubmissionField( $test ) === true );
 43+ }
 44+ }
 45+ }
 46+
 47+ /**
 48+ * Tests @see ContestDBObject::select and @see ContestDBObject::count
 49+ */
 50+ public function testObjectSelectCount() {
 51+ $classes = array( 'Contest', 'ContestChallenge' );
 52+
 53+ foreach ( $classes as $class ) {
 54+ $this->assertEquals( count( $class::s()->select() ), $class::s()->count() );
 55+ }
 56+ }
 57+
 58+}
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/test/ContestValidationTests.php
___________________________________________________________________
Added: svn:eol-style
159 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/Contest.i18n.php
@@ -0,0 +1,1059 @@
 2+<?php
 3+
 4+/**
 5+ * Internationalization file for the Contest extension.
 6+ *
 7+ * @since 0.1
 8+ *
 9+ * @file Contest.i18n.php
 10+ * @ingroup Contest
 11+ *
 12+ * @licence GNU GPL v3+
 13+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
 14+ */
 15+
 16+$messages = array();
 17+
 18+/** English
 19+ * @author Jeroen De Dauw
 20+ */
 21+$messages['en'] = array(
 22+ 'contest-desc' => 'Allows users to participate in contest challenges. Judges can discuss and vote on submissions',
 23+
 24+ // Misc
 25+ 'contest-toplink' => 'My contests',
 26+
 27+ // Rights
 28+ 'right-contestadmin' => 'Manage contests',
 29+ 'right-contestparticipant' => 'Participate in contests',
 30+ 'right-contestjudge' => 'Judge contest submissions',
 31+
 32+ // Groups
 33+ 'group-contestadmin' => 'Contest admins',
 34+ 'group-contestadmin-member' => '{{GENDER:$1|contest admin}}',
 35+ 'grouppage-contestadmin' => 'Project:Contest_admins',
 36+
 37+ 'group-contestparticipant' => 'Contest participants',
 38+ 'group-contestparticipant-member' => '{{GENDER:$1|contest participant}}',
 39+ 'grouppage-contestparticipant' => 'Project:Contest_participants',
 40+
 41+ 'group-contestjudge' => 'Contest judges',
 42+ 'group-contestjudge-member' => '{{GENDER:$1|contest judge}}',
 43+ 'grouppage-contestjudge' => 'Project:Contest_judges',
 44+
 45+ // Preferences
 46+ 'prefs-contest' => 'Contests',
 47+ 'contest-prefs-showtoplink' => 'Show a link to [[Special:MyContests|My Contests]] in the user menu.',
 48+
 49+ // Contest statuses
 50+ 'contest-status-draft' => 'Draft (disabled)',
 51+ 'contest-status-active' => 'Active (enabled)',
 52+ 'contest-status-expired' => 'Expired (enabled, past end date)',
 53+ 'contest-status-finished' => 'Finished (disabled)',
 54+
 55+ // Special page names
 56+ 'special-contest' => 'View a contest',
 57+ 'special-contests' => 'Manage contests',
 58+ 'special-contestsignup' => 'Sign up for a contest',
 59+ 'special-contestwelcome' => 'View a contest',
 60+ 'special-editcontest' => 'Edit a contest',
 61+ 'special-mycontests' => 'My contests',
 62+ 'specialpages-group-contest' => 'Contests',
 63+
 64+ // Navigation links
 65+ 'contest-nav-contests' => 'Contests list',
 66+ 'contest-nav-editcontest' => 'Edit contest',
 67+ 'contest-nav-contest' => 'Summary and participants',
 68+ 'contest-nav-contestwelcome' => 'Landing page',
 69+ 'contest-nav-contestsignup' => 'Signup page',
 70+
 71+ // Special:Contests
 72+ 'contest-special-addnew' => 'Add a new contest',
 73+ 'contest-special-namedoc' => 'The name of the contest is the identifier used in URLs. ie "name" in Special:Contest/name',
 74+ 'contest-special-newname' => 'Contest name',
 75+ 'contest-special-add' => 'Add contest',
 76+ 'contest-special-existing' => 'Existing contests',
 77+
 78+ 'contest-special-name' => 'Name',
 79+ 'contest-special-status' => 'Status',
 80+ 'contest-special-submissioncount' => 'Submission count',
 81+ 'contest-special-edit' => 'Edit',
 82+ 'contest-special-delete' => 'Delete',
 83+
 84+ 'contest-special-confirm-delete' => 'Are you sure you want to delete this contest?',
 85+ 'contest-special-delete-failed' => 'Failed to delete the contest.',
 86+
 87+ // Special:EditContest
 88+ 'editcontest-text' => 'You are editing a contest.',
 89+ 'editcontest-legend' => 'Contest',
 90+ 'contest-edit-name' => 'Contest name',
 91+ 'contest-edit-status' => 'Contest status',
 92+ 'contest-edit-intro' => 'Introduction page',
 93+ 'contest-edit-opportunities' => 'Opportunities page',
 94+ 'contest-edit-rulespage' => 'Rules page',
 95+ 'contest-edit-help' => 'Help page',
 96+ 'contest-edit-signup' => 'Signup e-mail page',
 97+ 'contest-edit-reminder' => 'Reminder e-mail page',
 98+ 'contest-edit-end' => 'Contest end',
 99+ 'contest-edit-exists-already' => 'Note: you are editing an already existing contest, not creating a new one.',
 100+ 'contest-edit-submit' => 'Submit',
 101+
 102+ 'contest-edit-challenges' => 'Contest challenges',
 103+ 'contest-edit-delete' => 'Delete challenge',
 104+ 'contest-edit-add-first' => 'Add a challenge',
 105+ 'contest-edit-add-another' => 'Add another challenge',
 106+ 'contest-edit-confirm-delete' => 'Are you sure you want to delete this challenge?',
 107+ 'contest-edit-challenge-title' => 'Challenge title',
 108+ 'contest-edit-challenge-text' => 'Challenge text',
 109+ 'contest-edit-challenge-oneline' => 'Short description',
 110+
 111+ // Special:ContestWelcome
 112+ 'contest-welcome-unknown' => 'There is no contest with the provided name.',
 113+ 'contest-welcome-rules' => 'In order to participate, you are required to agree to', // js i18n
 114+ 'contest-welcome-rules-link' => 'the contest rules', // js i18n
 115+ 'contest-welcome-signup' => 'Signup now',
 116+ 'contest-welcome-js-off' => 'The contest user interface uses JavaScript for an improved interface. Your browser either does not support JavaScript or has JavaScript turned off.',
 117+ 'contest-welcome-accept-challenge' => 'Challenge accepted',
 118+
 119+ 'contest-welcome-select-header' => 'Select your challenge:',
 120+
 121+ // Special:ContestSignup & Special:ContestSubmission
 122+ 'contest-signup-unknown' => 'There is no contest with the provided name.',
 123+ 'contest-signup-submit' => 'Sign up',
 124+ 'contest-signup-header' => 'Please fill out the form to complete your registration for $1.',
 125+ 'contest-signup-email' => 'Your e-mail address',
 126+ 'contest-signup-realname' => 'Your real name',
 127+ 'contest-signup-volunteer' => 'I am interested in volunteer opportunities',
 128+ 'contest-signup-wmf' => 'I am interested in working for the Wikimedia Foundation',
 129+ 'contest-signup-cv' => 'Link to your CV',
 130+ 'contest-signup-readrules' => 'I confirm that I have read, and agree to, [[$1|the contest rules]]',
 131+ 'contest-signup-challenge' => 'What challenge do you want to take on?',
 132+ 'contest-signup-finished' => 'This contest has ended. Thanks for your participation!',
 133+ 'contest-signup-draft' => 'This contest has not started yet. Please be patient.',
 134+ 'contest-signup-country' => 'Your country',
 135+
 136+ 'contest-signup-require-rules' => 'You need to agree to the contest rules.',
 137+ 'contest-signup-require-country' => 'You need to provide your country of residence.',
 138+ 'contest-signup-invalid-email' => 'The e-mail address you provided is not valid.',
 139+ 'contest-signup-invalid-name' => 'The name you provided is too short.',
 140+ 'contest-signup-require-challenge' => 'You must select a challenge.',
 141+ 'contest-signup-invalid-cv' => 'You entered an invalid URL.',
 142+
 143+ // Special:Contest
 144+ 'contest-contest-title' => 'Contest: $1',
 145+ 'contest-contest-no-results' => 'There are no contestants to display.',
 146+ 'contest-contest-name' => 'Name',
 147+ 'contest-contest-status' => 'Status',
 148+ 'contest-contest-submissioncount' => 'Amount of participants',
 149+ 'contest-contest-contestants' => 'Contest contestants',
 150+ 'contest-contest-contestants-text' => 'To judge an individual entry, click on the entry ID in the left column.',
 151+
 152+ // Contestant pager
 153+ 'contest-contestant-id' => 'ID',
 154+ 'contest-contestant-challenge-name' => 'Challenge name',
 155+ 'contest-contestant-volunteer' => 'Volunteer',
 156+ 'contest-contestant-wmf' => 'WMF',
 157+ 'contest-contestant-no' => 'No',
 158+ 'contest-contestant-yes' => 'Yes',
 159+ 'contest-contestant-commentcount' => 'Comments',
 160+ 'contest-contestant-overallrating' => 'Rating',
 161+ 'contest-contestant-rating' => '$1 ($2 {{PLURAL:$2|vote|votes}})',
 162+
 163+ // Special:Contestant
 164+ 'contest-contestant-title' => 'Contestant $1 ($2)',
 165+ 'contest-contestant-header-id' => 'Contestant ID',
 166+ 'contest-contestant-header-contest' => 'Contest name',
 167+ 'contest-contestant-header-challenge' => 'Challenge name',
 168+ 'contest-contestant-header-submission' => 'Submission link',
 169+ 'contest-contestant-header-country' => 'Contestant country',
 170+ 'contest-contestant-header-wmf' => 'Interested in WMF job',
 171+ 'contest-contestant-header-volunteer' => 'Interested in volunteer opportunities',
 172+ 'contest-contestant-header-rating' => 'Rating',
 173+ 'contest-contestant-header-comments' => 'Amount of comments',
 174+ 'contest-contestant-submission-url' => 'Submission',
 175+ 'contest-contestant-notsubmitted' => 'Not submitted yet',
 176+ 'contest-contestant-comments' => 'Comments',
 177+ 'contest-contestant-submit' => 'Save changes',
 178+ 'contest-contestant-comment-by' => 'Comment by $1',
 179+ 'contest-contestant-rate' => 'Rate this contestant',
 180+ 'contest-contestant-not-voted' => 'You have not voted on this participant yet.',
 181+ 'contest-contestant-voted' => 'Your current vote is $1.',
 182+ 'contest-contestant-permalink' => 'Permalink',
 183+
 184+ // Emails
 185+ 'contest-email-signup-title' => 'Thanks for joining the challenge!',
 186+ 'contest-email-reminder-title' => 'Only $1 {{PLURAL:$1|day|days}} until the end of the challenge!',
 187+
 188+ // Special:MyContests
 189+ 'contest-mycontests-toplink' => 'My contests',
 190+ 'contest-mycontests-no-contests' => 'You are not participating in any contest.',
 191+ 'contest-mycontests-active-header' => 'Running contests',
 192+ 'contest-mycontests-finished-header' => 'Passed contests',
 193+ 'contest-mycontests-active-text' => 'These are the contests you are currently participating in:',
 194+ 'contest-mycontests-finished-text' => 'These are the past contests you have participated in:',
 195+ 'contest-mycontests-header-contest' => 'Contest',
 196+ 'contest-mycontests-header-challenge' => 'Challenge',
 197+ 'contest-mycontests-signup-success' => 'You have successfully signed up for the $1 contest.',
 198+ 'contest-mycontests-addition-success' => 'You have successfully posted your submission! Thanks for participating in this contest.',
 199+ 'contest-mycontests-updated-success' => 'You have successfully modified your submission.',
 200+ 'contest-mycontests-sessionfail' => 'Your submission could not be saved due to loss of session data. Please try again.',
 201+
 202+ 'contest-submission-submit' => 'Submit',
 203+ 'contest-submission-unknown' => 'There is no contest with the provided name.',
 204+ 'contest-submission-header' => 'Thanks for participating in this contest! Once you have completed the challenge, you can add a link to your submission below.',
 205+ 'contest-submission-finished' => 'This contest has ended. Thanks for your participation!',
 206+
 207+ 'contest-submission-submission' => 'Link to your submission',
 208+ 'contest-submission-invalid-url' => 'This URL does not match one of the allowed formats.',
 209+ 'contest-submission-new-submission' => 'You still need to enter the URL to your submission. This needs to be done before the deadline.',
 210+ 'contest-submission-current-submission' => 'This is the URL to your submission, which you can modify untill the deadline.',
 211+
 212+ // TODO: how can this be done properly in JS?
 213+ 'contest-submission-domains' => 'Submissions are restricted to these sites: $1',
 214+);
 215+
 216+/** Message documentation (Message documentation)
 217+ * @author Jeroen De Dauw
 218+ */
 219+$messages['qqq'] = array(
 220+ 'contest-special-name' => 'Table column header',
 221+ 'contest-special-status' => 'Table column header',
 222+ 'contest-special-submissioncount' => 'Table column header',
 223+ 'contest-special-edit' => 'Table column header',
 224+ 'contest-special-delete' => 'Table column header',
 225+ 'editcontest-text' => 'Short text displayed at the top of the page notifying the user they are editing a contest',
 226+ 'contest-edit-name' => 'form field label',
 227+ 'contest-edit-status' => 'form field label',
 228+ 'contest-edit-intro' => 'Form field label',
 229+ 'contest-edit-opportunities' => 'Form field label',
 230+ 'contest-edit-rulespage' => 'Form field label',
 231+ 'contest-edit-help' => 'Form field label',
 232+ 'contest-edit-signup' => 'Form field label',
 233+ 'contest-edit-reminder' => 'Form field label',
 234+ 'contest-edit-end' => 'Form field label',
 235+ 'contest-edit-exists-already' => 'Warning message to show when the contest already exists',
 236+ 'contest-edit-submit' => 'Submit button text',
 237+ 'contest-edit-delete' => 'Delete challange button text',
 238+ 'contest-edit-add-first' => 'Add a challenge button text',
 239+ 'contest-edit-add-another' => 'Add another challenge button text',
 240+ 'contest-edit-confirm-delete' => 'Challange deletion confirmation message',
 241+ 'contest-edit-challenge-title' => 'Form field label',
 242+ 'contest-edit-challenge-text' => 'Form field label',
 243+ 'contest-edit-challenge-oneline' => 'Form field label',
 244+ 'contest-contest-title' => 'Page title',
 245+ 'contest-contest-no-results' => 'Message displayed instead of a table when there are no contests',
 246+ 'contest-contest-name' => 'Table row header',
 247+ 'contest-contest-status' => 'Table row header',
 248+ 'contest-contest-submissioncount' => 'Table row header',
 249+ 'contest-contest-contestants' => 'Page section header',
 250+ 'contest-contestant-id' => 'Table column header',
 251+ 'contest-contestant-volunteer' => 'Table column header',
 252+ 'contest-contestant-wmf' => 'Table column header',
 253+ 'contest-contestant-no' => 'Table cell value',
 254+ 'contest-contestant-yes' => 'Table cell value',
 255+ 'contest-contestant-commentcount' => 'Table column header',
 256+ 'contest-contestant-overallrating' => 'Table column header',
 257+ 'contest-contestant-rating' => '$1 is the avarage rating, $2 is the amount of votes',
 258+ 'contest-contestant-title' => 'Page title with contestant id $1 and contest name $2',
 259+ 'contest-contestant-header-id' => 'Table row header',
 260+ 'contest-contestant-header-contest' => 'Table row header',
 261+ 'contest-contestant-header-challenge' => 'Table row header',
 262+ 'contest-contestant-header-submission' => 'Table row header',
 263+ 'contest-contestant-header-country' => 'Table row header',
 264+ 'contest-contestant-header-wmf' => 'Table row header',
 265+ 'contest-contestant-header-volunteer' => 'Table row header',
 266+ 'contest-contestant-header-rating' => 'Table row header',
 267+ 'contest-contestant-header-comments' => 'Table row header',
 268+ 'contest-contestant-submission-url' => 'Text for the link to the submission',
 269+ 'contest-contestant-comments' => 'Page header (h2)',
 270+ 'contest-contestant-submit' => 'Submit button text',
 271+ 'contest-contestant-comment-by' => '$1 the user name, linked to the user page, followed by talk, contrib and block links',
 272+ 'contest-contestant-rate' => 'Page header (h2)',
 273+ 'contest-contestant-voted' => '$1 is an integer',
 274+ 'contest-contestant-permalink' => 'Hover-text for comment permalinks',
 275+ 'contest-email-signup-title' => 'Title for signup e-mails',
 276+ 'contest-email-reminder-title' => 'Title for reminder e-mails',
 277+
 278+ // Special:MyContests
 279+ 'contest-mycontests-toplink' => 'Text for link in the user menu (i.e. where watchlist and preferences are linked)',
 280+ 'contest-mycontests-no-contests' => 'Message indicating there are no contests for the user, displayed instead of a list.',
 281+ 'contest-mycontests-active-header' => 'Page header (h2)',
 282+ 'contest-mycontests-finished-header' => 'Page header (h2)',
 283+ 'contest-mycontests-active-text' => 'A list of all contests that a user is part of',
 284+ 'contest-mycontests-finished-text' => 'A list of all contests a user has been part of',
 285+ 'contest-mycontests-header-contest' => 'Contest',
 286+ 'contest-mycontests-header-challenge' => 'Header',
 287+ 'contest-mycontests-signup-success' => 'Message shown when a user successfully signs up for a contest. $1 is the name of the contest.',
 288+ 'contest-mycontests-addition-success' => 'Message shown when a user has added a submission',
 289+ 'contest-mycontests-updated-success' => 'Message shown when a user has editied a submission',
 290+ 'contest-mycontests-sessionfail' => 'Session failure',
 291+);
 292+
 293+/** German (Deutsch)
 294+ * @author Kghbln
 295+ */
 296+$messages['de'] = array(
 297+ 'contest-desc' => 'Ermöglicht Wettbewerbe sowie die anschließende Ermittlung der Gewinner durch Juroren',
 298+ 'contest-toplink' => 'Meine Wettbewerbe',
 299+ 'right-contestadmin' => 'Wettbewerbe verwalten',
 300+ 'right-contestparticipant' => 'An Wettbewerben teilnehmen',
 301+ 'right-contestjudge' => 'Wettbewerbsbeiträge beurteilen',
 302+ 'group-contestadmin' => 'Wettbewerbsadministratoren',
 303+ 'group-contestadmin-member' => '{{GENDER:$1|Wettbewerbsadministrator|Wettbewerbsadministratorin}}',
 304+ 'grouppage-contestadmin' => 'Project:Wettbewerbsadministratoren',
 305+ 'group-contestparticipant' => 'Wettbewerbsteilnehmer',
 306+ 'group-contestparticipant-member' => '{{GENDER:$1|Wettbewerbsteilnehmer|Wettbewerbsteilnehmerin}}',
 307+ 'grouppage-contestparticipant' => 'Project:Wettbewerbsteilnehmer',
 308+ 'group-contestjudge' => 'Wettbewerbsjuroren',
 309+ 'group-contestjudge-member' => '{{GENDER:$1|Wettbewerbsjuror|Wettbewerbsjurorin}}',
 310+ 'grouppage-contestjudge' => 'Project:Wettbewerbsjuroren',
 311+ 'prefs-contest' => 'Wettbewerbe',
 312+ 'contest-prefs-showtoplink' => 'Einen Link zu [[Special:MyContests|Meine Wettbewerbe]] im Benutzermenü anzeigen',
 313+ 'contest-status-draft' => 'Entwurf (deaktiviert)',
 314+ 'contest-status-active' => 'Aktiv (aktiviert)',
 315+ 'contest-status-expired' => 'Abgelaufen (aktiviert, Enddatum verstrichen)',
 316+ 'contest-status-finished' => 'Beendet (deaktiviert)',
 317+ 'special-contest' => 'Einen Wettbewerb ansehen',
 318+ 'special-contests' => 'Wettbewerbe verwalten',
 319+ 'special-contestsignup' => 'Zu einem Wettbewerb anmelden',
 320+ 'special-contestwelcome' => 'Einen Wettbewerb ansehen',
 321+ 'special-editcontest' => 'Einen Wettbewerb bearbeiten',
 322+ 'special-mycontests' => 'Meine Wettbewerbe',
 323+ 'specialpages-group-contest' => 'Wettbewerbe',
 324+ 'contest-nav-contests' => 'Wettbewerbsliste',
 325+ 'contest-nav-editcontest' => 'Wettbewerb bearbeiten',
 326+ 'contest-nav-contest' => 'Zusammenfassung und Teilnehmer',
 327+ 'contest-nav-contestwelcome' => 'Zielseite',
 328+ 'contest-nav-contestsignup' => 'Registrierungsseite',
 329+ 'contest-special-addnew' => 'Einen neuen Wettbewerb hinzufügen',
 330+ 'contest-special-namedoc' => 'Der Name des Wettbewerbs ist der Bezeichner, der in den URLs verwendet wird, bspw. „Name“ auf Spezial:Wettbewerbe/Name',
 331+ 'contest-special-newname' => 'Wettbewerbsname',
 332+ 'contest-special-add' => 'Wettbewerb hinzufügen',
 333+ 'contest-special-existing' => 'Vorhandene Wettbewerbe',
 334+ 'contest-special-name' => 'Name',
 335+ 'contest-special-status' => 'Status',
 336+ 'contest-special-submissioncount' => 'Wettbewerbsbeiträge',
 337+ 'contest-special-edit' => 'Bearbeiten',
 338+ 'contest-special-delete' => 'Löschen',
 339+ 'contest-special-confirm-delete' => 'Soll dieser Wettbewerb tatsächlich gelöscht werden?',
 340+ 'contest-special-delete-failed' => 'Der Wettbewerb konnte nicht gelöscht werden.',
 341+ 'editcontest-text' => 'Du bearbeitest einen Wettbewerb.',
 342+ 'editcontest-legend' => 'Wettbewerb',
 343+ 'contest-edit-name' => 'Wettbewerbsname',
 344+ 'contest-edit-status' => 'Wettbewerbsstatus',
 345+ 'contest-edit-intro' => 'Seite mit der Einführung',
 346+ 'contest-edit-opportunities' => 'Seite mit den Gewinnmöglichkeiten',
 347+ 'contest-edit-rulespage' => 'Seite mit den Regeln',
 348+ 'contest-edit-help' => 'Hilfeseite',
 349+ 'contest-edit-signup' => 'Seite mit der Anmeldungs-E-Mail',
 350+ 'contest-edit-reminder' => 'Seite mit der Erinnerungs-E-Mail',
 351+ 'contest-edit-end' => 'Wettbewerbsende',
 352+ 'contest-edit-exists-already' => 'Hinweis: Du erstellst gerade keinen neuen, sondern bearbeitest einen bereits vorhandenen Wettbewerb.',
 353+ 'contest-edit-submit' => 'Speichern',
 354+ 'contest-edit-challenges' => 'Wettbewerbsherausforderungen',
 355+ 'contest-edit-delete' => 'Herausforderung löschen',
 356+ 'contest-edit-add-first' => 'Eine Herausforderung hinzufügen',
 357+ 'contest-edit-add-another' => 'Eine weitere Herausforderung hinzufügen',
 358+ 'contest-edit-confirm-delete' => 'Soll diese Herausforderung tatsächlich gelöscht werden?',
 359+ 'contest-edit-challenge-title' => 'Name der Herausforderung',
 360+ 'contest-edit-challenge-text' => 'Text der Herausforderung',
 361+ 'contest-edit-challenge-oneline' => 'Kurzbeschreibung',
 362+ 'contest-welcome-unknown' => 'Es ist zum angegebenen Namen kein Wettbewerb vorhanden.',
 363+ 'contest-welcome-rules' => 'Um teilnehmen zu können, muss Folgendem zugestimmt werden:',
 364+ 'contest-welcome-rules-link' => 'die Wettbewerbsregeln',
 365+ 'contest-welcome-signup' => 'Jetzt registrieren',
 366+ 'contest-welcome-js-off' => 'Die Benutzeroberfläche der Wettbewerbe nutzt JavaScript für eine bessere Darstellung. Dein Browser unterstützt entweder kein JavaScript oder es wurde deaktiviert.',
 367+ 'contest-welcome-accept-challenge' => 'Herausforderung angenommen',
 368+ 'contest-welcome-select-header' => 'Herausforderung auswählen:',
 369+ 'contest-signup-unknown' => 'Es ist zum angegebenen Namen kein Wettbewerb vorhanden.',
 370+ 'contest-signup-submit' => 'Registrieren',
 371+ 'contest-signup-header' => 'Bitte das Formular zur Registrierung für $1 ausfüllen.',
 372+ 'contest-signup-email' => 'Deine E-Mail-Adresse',
 373+ 'contest-signup-realname' => 'Dein bürgerlicher Name',
 374+ 'contest-signup-volunteer' => 'Ich bin daran interessiert, mich als Freiwilliger zu beteiligen',
 375+ 'contest-signup-wmf' => 'Ich bin daran interessiert für die Wikimedia Foundation zu arbeiten',
 376+ 'contest-signup-cv' => 'Link zum Lebenslauf',
 377+ 'contest-signup-readrules' => 'Ich bestätige, dass ich [[$1|die Teilnahmebedingungen]] gelesen habe und ihnen zustimme',
 378+ 'contest-signup-challenge' => 'Welche Herausforderung möchtest du annehmen?',
 379+ 'contest-signup-finished' => 'Dieser Wettbewerb ist beendet. Vielen Dank für die Teilnahme.',
 380+ 'contest-signup-draft' => 'Der Wettbewerb hat noch nicht begonnen. Wir bitten um etwas Geduld.',
 381+ 'contest-signup-country' => 'Dein Land',
 382+ 'contest-signup-require-rules' => 'Du musst den Teilnahmebedingungen zustimmen.',
 383+ 'contest-signup-require-country' => 'Du muss das Land angeben, in dem du wohnst.',
 384+ 'contest-signup-invalid-email' => 'Die angegebene E-Mail-Adresse ist ungültig.',
 385+ 'contest-signup-invalid-name' => 'Der angegebene Name ist zu kurz.',
 386+ 'contest-signup-require-challenge' => 'Du musst eine Herausforderung annehmen.',
 387+ 'contest-signup-invalid-cv' => 'Die angegebene URL ist ungültig.',
 388+ 'contest-contest-title' => 'Wettbewerb: $1',
 389+ 'contest-contest-no-results' => 'Es gibt keine Teilnehmer zum Anzeigen.',
 390+ 'contest-contest-name' => 'Name',
 391+ 'contest-contest-status' => 'Status',
 392+ 'contest-contest-submissioncount' => 'Anzahl der Teilnehmer',
 393+ 'contest-contest-contestants' => 'Wettbewerbsteilnehmer',
 394+ 'contest-contest-contestants-text' => 'Um einen bestimmten Wettbewerbsbeitrag zu beurteilen, bitte auf die entsprechende Beitragskennung in der linken Spalte klicken.',
 395+ 'contest-contestant-id' => 'Kennung',
 396+ 'contest-contestant-challenge-name' => 'Name der Herausforderung',
 397+ 'contest-contestant-volunteer' => 'Freiwilliger',
 398+ 'contest-contestant-wmf' => 'WMF',
 399+ 'contest-contestant-no' => 'Nein',
 400+ 'contest-contestant-yes' => 'Ja',
 401+ 'contest-contestant-commentcount' => 'Kommentare',
 402+ 'contest-contestant-overallrating' => 'Bewertung',
 403+ 'contest-contestant-rating' => '$1 ($2 {{PLURAL:$2|Stimme|Stimmen}})',
 404+ 'contest-contestant-title' => 'Teilnehmer $1 ($2)',
 405+ 'contest-contestant-header-id' => 'Teilnehmerkennung',
 406+ 'contest-contestant-header-contest' => 'Wettbewerbsname',
 407+ 'contest-contestant-header-challenge' => 'Name der Herausforderung',
 408+ 'contest-contestant-header-submission' => 'Link zum Beitrag',
 409+ 'contest-contestant-header-country' => 'Teilnehmerland',
 410+ 'contest-contestant-header-wmf' => 'An einer Anstellung bei der WMF interessiert',
 411+ 'contest-contestant-header-volunteer' => 'An freiwilliger Mitarbeit interessiert',
 412+ 'contest-contestant-header-rating' => 'Bewertung',
 413+ 'contest-contestant-header-comments' => 'Anzahl der Kommentare',
 414+ 'contest-contestant-submission-url' => 'Beitrag',
 415+ 'contest-contestant-notsubmitted' => 'Noch nicht eingereicht',
 416+ 'contest-contestant-comments' => 'Kommentare',
 417+ 'contest-contestant-submit' => 'Änderungen speichern',
 418+ 'contest-contestant-comment-by' => 'Kommentar von $1',
 419+ 'contest-contestant-rate' => 'Diese Teilnehmer bewerten',
 420+ 'contest-contestant-not-voted' => 'Du hast noch nicht bezüglich dieses Teilnehmers abgestimmt.',
 421+ 'contest-contestant-voted' => 'Deine aktuelle Stimmenanzahl beträgt $1.',
 422+ 'contest-contestant-permalink' => 'Permanentlink',
 423+ 'contest-email-signup-title' => 'Vielen Dank für die Teilnahme an dieser Herausforderung.',
 424+ 'contest-email-reminder-title' => 'Nur $1 {{PLURAL:$1|Tag|Tage}} bis zum Ablauf des Herausforderungszeitraums.',
 425+ 'contest-mycontests-toplink' => 'Meine Wettbewerbe',
 426+ 'contest-mycontests-no-contests' => 'Du nimmst an keinem Wettbewerb teil.',
 427+ 'contest-mycontests-active-header' => 'Laufende Wettbewerbe',
 428+ 'contest-mycontests-finished-header' => 'Beendete Wettbewerbe',
 429+ 'contest-mycontests-active-text' => 'Dies sind die Wettbewerbe, an denen du derzeit teilnimmst.',
 430+ 'contest-mycontests-finished-text' => 'Dies sind die beendeten Wettbewerbe an denen du teilgenommen hast.',
 431+ 'contest-mycontests-header-contest' => 'Wettbewerb',
 432+ 'contest-mycontests-header-challenge' => 'Herausforderung',
 433+ 'contest-mycontests-signup-success' => 'Du hast dich erfolgreich für diesen Wettbewerb angemeldet.',
 434+ 'contest-mycontests-addition-success' => 'Du hast deinen Beitrag erfolgreich eingereicht. Vielen Dank für die Teilnehme an diesem Wettbewerb.',
 435+ 'contest-mycontests-updated-success' => 'Du hast deinen Beitrag erfolgreich geändert.',
 436+ 'contest-mycontests-sessionfail' => 'Dein Beitrag konnte aufgrund verlorengegangener Sitzungsdaten nicht gespeichert werden. Bitte versuche es erneut.',
 437+ 'contest-submission-submit' => 'Speichern',
 438+ 'contest-submission-unknown' => 'Es ist zum angegebenen Namen kein Wettbewerb vorhanden.',
 439+ 'contest-submission-header' => 'Vielen Dank für deine Teilnahme an diesem Wettbewerb. Nachdem du deine Herausforderung bewältigt hast, kannst du unten den Link zu deinem Beitrag hinzufügen.',
 440+ 'contest-submission-finished' => 'Dieser Wettbewerb ist beendet. Vielen Dank für die Teilnahme.',
 441+ 'contest-submission-submission' => 'Link zu deinem Beitrag',
 442+ 'contest-submission-invalid-url' => 'Diese URL entspricht nicht einem der zulässigen Formate.',
 443+ 'contest-submission-new-submission' => 'Du musst noch den Link zu deinem Beitrag hinzufügen. Dies muss noch vor Ablauf der Wettbewerbsfrist erfolgen.',
 444+ 'contest-submission-current-submission' => 'Dies ist die URL zu deinem Beitrag. Du kannst ihn noch bis zum Ende der Wettbewerbsfrist ändern.',
 445+ 'contest-submission-domains' => 'Beiträge sind auf diese Websites beschränkt: $1',
 446+);
 447+
 448+/** German (formal address) (‪Deutsch (Sie-Form)‬)
 449+ * @author Kghbln
 450+ */
 451+$messages['de-formal'] = array(
 452+ 'editcontest-text' => 'Sie bearbeiten einen Wettbewerb.',
 453+ 'contest-edit-exists-already' => 'Hinweis: Sie erstellen gerade keinen neuen, sondern bearbeiten einen bereits vorhandenen Wettbewerb.',
 454+ 'contest-welcome-js-off' => 'Die Benutzeroberfläche der Wettbewerbe nutzt JavaScript für eine bessere Darstellung. Ihr Browser unterstützt entweder kein JavaScript oder es wurde deaktiviert.',
 455+ 'contest-signup-email' => 'Ihre E-Mail-Adresse',
 456+ 'contest-signup-realname' => 'Ihr bürgerlicher Name',
 457+ 'contest-signup-challenge' => 'Welche Herausforderung möchten Sie annehmen?',
 458+ 'contest-signup-country' => 'Ihr Land',
 459+ 'contest-signup-require-rules' => 'Sie müssen den Teilnahmebedingungen zustimmen.',
 460+ 'contest-signup-require-country' => 'Sie müssen das Land angeben, in dem Sie wohnen.',
 461+ 'contest-signup-require-challenge' => 'Sie müssen eine Herausforderung annehmen.',
 462+ 'contest-contestant-not-voted' => 'Sie haben noch nicht bezüglich dieses Teilnehmers abgestimmt.',
 463+ 'contest-contestant-voted' => 'Ihre aktuelle Stimmenanzahl beträgt $1.',
 464+ 'contest-mycontests-no-contests' => 'Sie nehmen an keinem Wettbewerb teil.',
 465+ 'contest-mycontests-active-text' => 'Dies sind die Wettbewerbe, an denen Sie derzeit teilnehmen.',
 466+ 'contest-mycontests-finished-text' => 'Dies sind die beendeten Wettbewerbe an denen Sie teilgenommen haben.',
 467+ 'contest-mycontests-signup-success' => 'Sie haben sich erfolgreich für diesen Wettbewerb angemeldet.',
 468+ 'contest-mycontests-addition-success' => 'Sie haben Ihren Beitrag erfolgreich eingereicht. Vielen Dank für die Teilnehme an diesem Wettbewerb.',
 469+ 'contest-mycontests-updated-success' => 'Sie haben Ihren Beitrag erfolgreich geändert.',
 470+ 'contest-mycontests-sessionfail' => 'Ihr Beitrag konnte aufgrund verlorengegangener Sitzungsdaten nicht gespeichert werden. Bitte versuchen Sie es erneut.',
 471+ 'contest-submission-header' => 'Vielen Dank für Ihre Teilnahme an diesem Wettbewerb. Nachdem Sie Ihre Herausforderung bewältigt haben, können Sie unten den Link zu Ihrem Beitrag hinzufügen.',
 472+ 'contest-submission-submission' => 'Link zu Ihrem Beitrag',
 473+ 'contest-submission-new-submission' => 'Sie müssen noch den Link zu Ihrem Beitrag hinzufügen. Dies muss noch vor Ablauf der Wettbewerbsfrist erfolgen.',
 474+ 'contest-submission-current-submission' => 'Dies ist die URL zu Ihrem Beitrag. Sie können ihn noch bis zum Ende der Wettbewerbsfrist ändern.',
 475+);
 476+
 477+/** French (Français)
 478+ * @author Gomoko
 479+ */
 480+$messages['fr'] = array(
 481+ 'contest-desc' => 'Extension de concours qui permet aux utilisateurs de participer à des défis de concours organisés par un administrateur. Via une interface de jugement, les juges peuvent discuter et voter sur les propositions.',
 482+ 'contest-toplink' => 'Mes concours',
 483+ 'right-contestadmin' => 'Gérer les concours',
 484+ 'right-contestparticipant' => 'Participer aux concours',
 485+ 'right-contestjudge' => 'Juger les réponses au concours',
 486+ 'group-contestadmin' => 'Administrateurs du concours',
 487+ 'group-contestadmin-member' => '{{GENDER:$1|administrateur du concours}}',
 488+ 'grouppage-contestadmin' => 'Project:Contest_admins',
 489+ 'group-contestparticipant' => 'Participants au concours',
 490+ 'group-contestparticipant-member' => '{{GENDER:$1|participant au concours}}',
 491+ 'grouppage-contestparticipant' => 'Project:Contest_participants',
 492+ 'group-contestjudge' => 'Juges du concours',
 493+ 'group-contestjudge-member' => '{{GENDER:$1|juge du concours}}',
 494+ 'grouppage-contestjudge' => 'Project:Contest_judges',
 495+ 'prefs-contest' => 'Concours',
 496+ 'contest-prefs-showtoplink' => 'Afficher un lien vers [[Special:MyContests|Mes Concours]] dans le menu principal.',
 497+ 'contest-status-draft' => 'Brouillon (désactivé)',
 498+ 'contest-status-active' => 'Actif (activé)',
 499+ 'contest-status-expired' => 'Expiré (activé, date de fin dépassée)',
 500+ 'contest-status-finished' => 'Terminé (désactivé)',
 501+ 'special-contest' => 'Voir un concours',
 502+ 'special-contests' => 'Gérer les concours',
 503+ 'special-contestsignup' => "S'inscrire à un concours",
 504+ 'special-contestwelcome' => 'Afficher un concours',
 505+ 'special-editcontest' => 'Modifier un concours',
 506+ 'special-mycontests' => 'Mes concours',
 507+ 'specialpages-group-contest' => 'Concours',
 508+ 'contest-nav-contests' => 'Liste des concours',
 509+ 'contest-nav-editcontest' => 'Modifier un concours',
 510+ 'contest-nav-contest' => 'Résumé et participants',
 511+ 'contest-nav-contestwelcome' => 'Page de destination',
 512+ 'contest-nav-contestsignup' => 'Page de connexion',
 513+ 'contest-special-addnew' => 'Ajouter un nouveau concours',
 514+ 'contest-special-namedoc' => 'Le nom du concours est l\'identifiant utilisé dans les URLs, soit "nom" dans Special:Contest/nom',
 515+ 'contest-special-newname' => 'Nom du concours',
 516+ 'contest-special-add' => 'Ajouter un concours',
 517+ 'contest-special-existing' => 'Concours existants',
 518+ 'contest-special-name' => 'Nom',
 519+ 'contest-special-status' => 'Statut',
 520+ 'contest-special-submissioncount' => 'Nombre de réponses',
 521+ 'contest-special-edit' => 'Modifier',
 522+ 'contest-special-delete' => 'Supprimer',
 523+ 'contest-special-confirm-delete' => 'Êtes-vous sûrs de vouloir supprimer ce concours?',
 524+ 'contest-special-delete-failed' => 'Échec à la suppression du concours.',
 525+ 'editcontest-text' => 'Vous êtes en train de modifier un concours.',
 526+ 'editcontest-legend' => 'Concours',
 527+ 'contest-edit-name' => 'Nom du concours',
 528+ 'contest-edit-status' => 'Statut du concours',
 529+ 'contest-edit-intro' => "Page d'introduction",
 530+ 'contest-edit-opportunities' => 'Page des possibilités',
 531+ 'contest-edit-rulespage' => 'Page des règles',
 532+ 'contest-edit-help' => "Page d'aide",
 533+ 'contest-edit-signup' => "Page du courriel d'abonnement",
 534+ 'contest-edit-reminder' => 'Page du courriel de rappel',
 535+ 'contest-edit-end' => 'Fin du concours',
 536+ 'contest-edit-exists-already' => "Note: vous êtes en train de modifier un concours déjà existant, non pas d'en créer un nouveau.",
 537+ 'contest-edit-submit' => 'Soumettre',
 538+ 'contest-edit-challenges' => 'Défis du concours',
 539+ 'contest-edit-delete' => 'Supprimer un défi',
 540+ 'contest-edit-add-first' => 'Ajouter un défi',
 541+ 'contest-edit-add-another' => 'Ajouter un autre défi',
 542+ 'contest-edit-confirm-delete' => 'Êtes-vous sûr de vouloir supprimer ce défi?',
 543+ 'contest-edit-challenge-title' => 'Titre du défi',
 544+ 'contest-edit-challenge-text' => 'Texte du défi',
 545+ 'contest-edit-challenge-oneline' => 'Description courte',
 546+ 'contest-welcome-unknown' => "Il n'y a pas de concours avec le nom indiqué.",
 547+ 'contest-welcome-rules' => 'Afin de participer, vous devez accepter',
 548+ 'contest-welcome-rules-link' => 'les règles du concours',
 549+ 'contest-welcome-signup' => "S'inscrire maintenant",
 550+ 'contest-welcome-js-off' => "L'interface utilisateur du concours utilise JavaScript pour une interface améliorée. Votre navigateur, soit ne supporte pas JavaScript, soit a JavaScript désactivé.",
 551+ 'contest-welcome-accept-challenge' => 'Défi accepté',
 552+ 'contest-welcome-select-header' => 'Choisir votre défi:',
 553+ 'contest-signup-unknown' => "Il n'y a pas de concours avec le nom indiqué.",
 554+ 'contest-signup-submit' => "S'inscrire",
 555+ 'contest-signup-header' => 'Merci de remplir le formulaire pour terminer votre inscription pour $1.',
 556+ 'contest-signup-email' => 'Votre adresse de courriel',
 557+ 'contest-signup-realname' => 'Votre vrai nom',
 558+ 'contest-signup-volunteer' => 'Je suis intéressé par les possibilités de bénévolat',
 559+ 'contest-signup-wmf' => 'Je suis intéresse pour travailler pour la Fondation Wikimedia',
 560+ 'contest-signup-cv' => 'Lien vers votre CV',
 561+ 'contest-signup-readrules' => "Je confirme que j'ai lu et approuvé [[$1|les règles du concours]]",
 562+ 'contest-signup-challenge' => 'Quel défi voulez-vous relever?',
 563+ 'contest-signup-finished' => 'Ce concours est terminé. Merci pour votre participation!',
 564+ 'contest-signup-draft' => "Ce concours n'a pas encore commencé. Merci de patienter.",
 565+ 'contest-signup-country' => 'Votre pays',
 566+ 'contest-signup-require-rules' => 'Vous devez accepter les règles du concours.',
 567+ 'contest-signup-require-country' => 'Vous devez indiquer votre pays de résidence.',
 568+ 'contest-signup-invalid-email' => "L'adresse de courriel que vous avez fournie n'est pas valide.",
 569+ 'contest-signup-invalid-name' => 'Le nom que vous avez indiqué est trop court.',
 570+ 'contest-signup-require-challenge' => 'Vous devez sélectionner un défi.',
 571+ 'contest-signup-invalid-cv' => 'Vous avez entré une mauvaise URL.',
 572+ 'contest-contest-title' => 'Concours: $1',
 573+ 'contest-contest-no-results' => "Il n'y a pas de participants à afficher.",
 574+ 'contest-contest-name' => 'Nom',
 575+ 'contest-contest-status' => 'Statut',
 576+ 'contest-contest-submissioncount' => 'Nombre de participants',
 577+ 'contest-contest-contestants' => 'Participants au concours',
 578+ 'contest-contest-contestants-text' => "Pour juger une proposition individuelle, cliquez sur l'ID de celle-ci dans la colonne de gauche.",
 579+ 'contest-contestant-id' => 'ID',
 580+ 'contest-contestant-challenge-name' => 'Nom du défi',
 581+ 'contest-contestant-volunteer' => 'Bénévole',
 582+ 'contest-contestant-wmf' => 'WMF',
 583+ 'contest-contestant-no' => 'Non',
 584+ 'contest-contestant-yes' => 'Oui',
 585+ 'contest-contestant-commentcount' => 'Commentaires',
 586+ 'contest-contestant-overallrating' => 'Évaluation',
 587+ 'contest-contestant-rating' => '$1 ($2 {{PLURAL:$2|vote|votes}})',
 588+ 'contest-contestant-title' => 'Candidat $1 ($2)',
 589+ 'contest-contestant-header-id' => 'ID du candidat',
 590+ 'contest-contestant-header-contest' => 'Nom du concours',
 591+ 'contest-contestant-header-challenge' => 'Nom du défi',
 592+ 'contest-contestant-header-submission' => 'Lien de réponse',
 593+ 'contest-contestant-header-country' => 'Pays du candidat',
 594+ 'contest-contestant-header-wmf' => 'Intéressé par un travail WMF',
 595+ 'contest-contestant-header-volunteer' => 'Intéressé par les possibilités de bénévolat',
 596+ 'contest-contestant-header-rating' => 'Évaluation',
 597+ 'contest-contestant-header-comments' => 'Nombre de commentaires',
 598+ 'contest-contestant-submission-url' => 'Réponse',
 599+ 'contest-contestant-notsubmitted' => 'Pas encore envoyé',
 600+ 'contest-contestant-comments' => 'Commentaires',
 601+ 'contest-contestant-submit' => 'Enregistrer les modifications',
 602+ 'contest-contestant-comment-by' => 'Commentaire par $1',
 603+ 'contest-contestant-rate' => 'Évaluer ce candidat',
 604+ 'contest-contestant-not-voted' => "Vous n'avez pas encore voté pour ce candidat.",
 605+ 'contest-contestant-voted' => 'Votre vote actuel est $1.',
 606+ 'contest-contestant-permalink' => 'Lien permanent',
 607+ 'contest-email-signup-title' => "Merci d'avoir rejoint le défi!",
 608+ 'contest-email-reminder-title' => 'Plus que $1 {{PLURAL:$1|jour|jours}} avant la fin du défi!',
 609+ 'contest-mycontests-toplink' => 'Mes concours',
 610+ 'contest-mycontests-no-contests' => 'Vous ne participez à aucun concours.',
 611+ 'contest-mycontests-active-header' => 'Concours en cours',
 612+ 'contest-mycontests-finished-header' => 'Concours terminés',
 613+ 'contest-mycontests-active-text' => 'Voici les concours auxquels vous participez actuellement.',
 614+ 'contest-mycontests-finished-text' => 'Voici les concours terminés auxquels vous avez participé.',
 615+ 'contest-mycontests-header-contest' => 'Concours',
 616+ 'contest-mycontests-header-challenge' => 'Défi',
 617+ 'contest-mycontests-signup-success' => 'Vous avez réussi à vous inscrire à ce concours.',
 618+ 'contest-mycontests-addition-success' => 'Vous avez réussi à envoyer votre réponse! Merci de votre participation à ce concours.',
 619+ 'contest-mycontests-updated-success' => 'Vous avez réussi à modifier votre réponse.',
 620+ 'contest-mycontests-sessionfail' => "Votre réponse n'a pas pu être enregistrée à cause d'une perte des données de session. Merci d'essayer de nouveau.",
 621+ 'contest-submission-submit' => 'Soumettre',
 622+ 'contest-submission-unknown' => "Il n'y a pas de concours avec le nom indiqué.",
 623+ 'contest-submission-header' => "Merci d'avoir participé à ce concours! Une fois votre défi terminé, vous pouvez ajouter un lien à votre réponse ci-dessous.",
 624+ 'contest-submission-finished' => 'Ce concours est terminé. Merci pour votre participation!',
 625+ 'contest-submission-submission' => 'Lien vers votre réponse',
 626+ 'contest-submission-invalid-url' => 'Cette URL ne correspond pas à un des formats autorisés.',
 627+ 'contest-submission-new-submission' => "Vous devez encore entrer l'URL de votre réponse. Cela doit être fait avant la date limite.",
 628+ 'contest-submission-current-submission' => "Voici l'URL de votre réponse, que vous pouvez modifier jusqu'à la date limite.",
 629+ 'contest-submission-domains' => 'Les réponses sont limitées à ces sites: $1',
 630+);
 631+
 632+/** Interlingua (Interlingua)
 633+ * @author McDutchie
 634+ */
 635+$messages['ia'] = array(
 636+ 'contest-desc' => 'Permitte al usatores de participar in concursos definite per administratores. Via un interfacie de judicamento, le judices pote discuter e votar sur submissiones.',
 637+ 'contest-toplink' => 'Mi concursos',
 638+ 'right-contestadmin' => 'Gerer concursos',
 639+ 'right-contestparticipant' => 'Participar in concursos',
 640+ 'right-contestjudge' => 'Judicar submissiones de concurso',
 641+ 'group-contestadmin' => 'Administratores de concurso',
 642+ 'group-contestadmin-member' => '{{GENDER:$1|administrator|administratrice}} de concurso',
 643+ 'grouppage-contestadmin' => 'Project:Administratores de concurso',
 644+ 'group-contestparticipant' => 'Participantes del concurso',
 645+ 'group-contestparticipant-member' => '{{GENDER:$1|participante}} de concurso',
 646+ 'grouppage-contestparticipant' => 'Project:Participantes de concurso',
 647+ 'group-contestjudge' => 'Judices de concurso',
 648+ 'group-contestjudge-member' => '{{GENDER:$1|judice}} de concurso',
 649+ 'grouppage-contestjudge' => 'Project:Judices de concurso',
 650+ 'prefs-contest' => 'Concursos',
 651+ 'contest-prefs-showtoplink' => 'Monstrar un ligamine a [[Special:MyContests|Mi concursos]] in le menu al initio.',
 652+ 'contest-status-draft' => 'Version provisori (disactivate)',
 653+ 'contest-status-active' => 'Active (activate)',
 654+ 'contest-status-expired' => 'Expirate (activate, data de fin passate)',
 655+ 'contest-status-finished' => 'Finite (disactivate)',
 656+ 'special-contest' => 'Vider un concurso',
 657+ 'special-contests' => 'Gerer concursos',
 658+ 'special-contestsignup' => 'Inscriber se pro un concurso',
 659+ 'special-contestwelcome' => 'Vider un concurso',
 660+ 'special-editcontest' => 'Modificar un concurso',
 661+ 'special-mycontests' => 'Mi concursos',
 662+ 'specialpages-group-contest' => 'Concursos',
 663+ 'contest-nav-contests' => 'Lista de concursos',
 664+ 'contest-nav-editcontest' => 'Modificar concurso',
 665+ 'contest-nav-contest' => 'Summario e participantes',
 666+ 'contest-nav-contestwelcome' => 'Pagina de arrivata',
 667+ 'contest-nav-contestsignup' => 'Pagina de inscription',
 668+ 'contest-special-addnew' => 'Adder un nove concurso',
 669+ 'contest-special-namedoc' => 'Le nomine del concurso es le identificator usate in URLs, i.e. "nomine" in Special:Concurso/nomine',
 670+ 'contest-special-newname' => 'Nomine del concurso',
 671+ 'contest-special-add' => 'Adder concurso',
 672+ 'contest-special-existing' => 'Concursos existente',
 673+ 'contest-special-name' => 'Nomine',
 674+ 'contest-special-status' => 'Stato',
 675+ 'contest-special-submissioncount' => 'Numero de submissiones',
 676+ 'contest-special-edit' => 'Modificar',
 677+ 'contest-special-delete' => 'Deler',
 678+ 'contest-special-confirm-delete' => 'Es tu secur de voler deler iste concurso?',
 679+ 'contest-special-delete-failed' => 'Le deletion del concurso ha fallite.',
 680+ 'editcontest-text' => 'Tu modifica ora un concurso.',
 681+ 'editcontest-legend' => 'Concurso',
 682+ 'contest-edit-name' => 'Nomine del concurso',
 683+ 'contest-edit-status' => 'Stato del concurso',
 684+ 'contest-edit-intro' => 'Pagina de introduction',
 685+ 'contest-edit-opportunities' => 'Pagina de opportunitates',
 686+ 'contest-edit-rulespage' => 'Pagina de regulas',
 687+ 'contest-edit-help' => 'Pagina de adjuta',
 688+ 'contest-edit-signup' => 'Pagina de e-mail de inscription',
 689+ 'contest-edit-reminder' => 'Pagina de e-mail de rememoration',
 690+ 'contest-edit-end' => 'Fin del concurso',
 691+ 'contest-edit-exists-already' => 'Nota: ora tu modifica un concurso jam existente, tu non crea un nove.',
 692+ 'contest-edit-submit' => 'Submitter',
 693+ 'contest-edit-challenges' => 'Defias de concurso',
 694+ 'contest-edit-delete' => 'Deler defia',
 695+ 'contest-edit-add-first' => 'Adder un defia',
 696+ 'contest-edit-add-another' => 'Adder un altere defia',
 697+ 'contest-edit-confirm-delete' => 'Es tu secur de voler deler iste defia?',
 698+ 'contest-edit-challenge-title' => 'Titulo del defia',
 699+ 'contest-edit-challenge-text' => 'Texto del defia',
 700+ 'contest-edit-challenge-oneline' => 'Curte description',
 701+ 'contest-welcome-unknown' => 'Non existe un concurso con le nomine specificate.',
 702+ 'contest-welcome-rules' => 'Pro poter participar, es necessari declarar se de accordo con',
 703+ 'contest-welcome-rules-link' => 'le regulas del concurso',
 704+ 'contest-welcome-signup' => 'Inscriber ora',
 705+ 'contest-welcome-js-off' => 'Le interfacie de usator del concurso usa JavaScript pro un experientia melior. Ma tu navigator non supporta JavaScript o lo ha disactivate.',
 706+ 'contest-welcome-accept-challenge' => 'Defia acceptate',
 707+ 'contest-welcome-select-header' => 'Selige tu defia:',
 708+ 'contest-signup-unknown' => 'Non existe un concurso con le nomine specificate.',
 709+ 'contest-signup-submit' => 'Inscriber',
 710+ 'contest-signup-header' => 'Per favor plena le formulario pro completar le inscription pro $1.',
 711+ 'contest-signup-email' => 'Adresse de e-mail',
 712+ 'contest-signup-realname' => 'Nomine real',
 713+ 'contest-signup-volunteer' => 'Io ha interesse in devenir voluntario',
 714+ 'contest-signup-wmf' => 'Io ha interesse in laborar pro le Fundation Wikimedia',
 715+ 'contest-signup-cv' => 'Ligamine a tu CV',
 716+ 'contest-signup-readrules' => 'Io confirma que io ha legite, e es de accordo con, [[$1|le regulas del concurso]]',
 717+ 'contest-signup-challenge' => 'Qual defia vole tu acceptar?',
 718+ 'contest-signup-finished' => 'Iste concurso ha finite. Gratias pro haber participate!',
 719+ 'contest-signup-draft' => 'Iste concurso non ha ancora comenciate. Per favor sia patiente.',
 720+ 'contest-signup-country' => 'Pais',
 721+ 'contest-signup-require-rules' => 'Es necessari declarar se de accordo con le regulas del concurso.',
 722+ 'contest-signup-require-country' => 'Es necessari fornir le pais ubi tu reside.',
 723+ 'contest-signup-invalid-email' => 'Le adresse de e-mail que tu ha specificate non es valide.',
 724+ 'contest-signup-invalid-name' => 'Le nomine que tu ha specificate es troppo curte.',
 725+ 'contest-signup-require-challenge' => 'Es necessari seliger un defia.',
 726+ 'contest-signup-invalid-cv' => 'Tu ha specificate un URL invalide.',
 727+ 'contest-contest-title' => 'Concurso: $1',
 728+ 'contest-contest-no-results' => 'Il non ha concurrentes a monstrar.',
 729+ 'contest-contest-name' => 'Nomine',
 730+ 'contest-contest-status' => 'Stato',
 731+ 'contest-contest-submissioncount' => 'Numero de participantes',
 732+ 'contest-contest-contestants' => 'Concurrentes del concurso',
 733+ 'contest-contest-contestants-text' => 'Pro judicar un entrata individual, clicca sur le ID del entrata in le columna sinistre.',
 734+ 'contest-contestant-id' => 'ID',
 735+ 'contest-contestant-challenge-name' => 'Nomine del defia',
 736+ 'contest-contestant-volunteer' => 'Voluntario',
 737+ 'contest-contestant-wmf' => 'WMF',
 738+ 'contest-contestant-no' => 'No',
 739+ 'contest-contestant-yes' => 'Si',
 740+ 'contest-contestant-commentcount' => 'Commentos',
 741+ 'contest-contestant-overallrating' => 'Evalutation',
 742+ 'contest-contestant-rating' => '$1 ($2 {{PLURAL:$2|voto|votos}})',
 743+ 'contest-contestant-title' => 'Concurrente $1 ($2)',
 744+ 'contest-contestant-header-id' => 'ID de concurrente',
 745+ 'contest-contestant-header-contest' => 'Nomine de concurso',
 746+ 'contest-contestant-header-challenge' => 'Nomine de defia',
 747+ 'contest-contestant-header-submission' => 'Ligamine de submission',
 748+ 'contest-contestant-header-country' => 'Pais de concurrente',
 749+ 'contest-contestant-header-wmf' => 'Interesse in empleo a WMF',
 750+ 'contest-contestant-header-volunteer' => 'Interesse in devenir voluntario',
 751+ 'contest-contestant-header-rating' => 'Evalutation',
 752+ 'contest-contestant-header-comments' => 'Numero de commentos',
 753+ 'contest-contestant-submission-url' => 'Submission',
 754+ 'contest-contestant-notsubmitted' => 'Non ancora submittite',
 755+ 'contest-contestant-comments' => 'Commentos',
 756+ 'contest-contestant-submit' => 'Salveguardar modificationes',
 757+ 'contest-contestant-comment-by' => 'Commento per $1',
 758+ 'contest-contestant-rate' => 'Evalutar iste concurrente',
 759+ 'contest-contestant-not-voted' => 'Tu non ha ancora votate sur iste participante.',
 760+ 'contest-contestant-voted' => 'Tu voto actual es $1.',
 761+ 'contest-contestant-permalink' => 'Permaligamine',
 762+ 'contest-email-signup-title' => 'Gratias pro acceptar le defia!',
 763+ 'contest-email-reminder-title' => 'Solmente $1 {{PLURAL:$1|die|dies}} usque le fin del defia!',
 764+ 'contest-mycontests-toplink' => 'Mi concursos',
 765+ 'contest-mycontests-no-contests' => 'Tu non participa in un concurso.',
 766+ 'contest-mycontests-active-header' => 'Concursos currente',
 767+ 'contest-mycontests-finished-header' => 'Concursos passate',
 768+ 'contest-mycontests-active-text' => 'Istes es le concursos in le quales tu participa actualmente.',
 769+ 'contest-mycontests-finished-text' => 'Istes es le concursos passate in le quales tu ha participate.',
 770+ 'contest-mycontests-header-contest' => 'Concurso',
 771+ 'contest-mycontests-header-challenge' => 'Defia',
 772+ 'contest-mycontests-signup-success' => 'Tu te ha inscribite pro iste concurso con successo.',
 773+ 'contest-mycontests-addition-success' => 'Tu ha inviate le submission con successo! Gratias pro participar in iste concurso.',
 774+ 'contest-mycontests-updated-success' => 'Tu ha modificate le submission con successo.',
 775+ 'contest-mycontests-sessionfail' => 'Le submission non poteva esser salveguardate proque le datos del session es perdite. Per favor reproba.',
 776+ 'contest-submission-submit' => 'Submitter',
 777+ 'contest-submission-unknown' => 'Non existe un concurso con le nomine specificate.',
 778+ 'contest-submission-header' => 'Gratias pro participar in iste concurso! Un vice que tu ha completate le defia, tu pote adder un ligamine a tu submission hic infra.',
 779+ 'contest-submission-finished' => 'Iste concurso ha finite. Gratias pro haber participate!',
 780+ 'contest-submission-submission' => 'Ligamine a tu submission',
 781+ 'contest-submission-invalid-url' => 'Iste URL non corresponde a un del formatos permittite.',
 782+ 'contest-submission-new-submission' => 'Tu debe ancora specificar le URL a tu submission. Isto debe esser facite ante le data limite.',
 783+ 'contest-submission-current-submission' => 'Isto es le URL a tu submission, le qual tu pote modificar usque le data limite.',
 784+ 'contest-submission-domains' => 'Le submissiones es limitate a iste sitos: $1',
 785+);
 786+
 787+/** Macedonian (Македонски)
 788+ * @author Bjankuloski06
 789+ */
 790+$messages['mk'] = array(
 791+ 'contest-desc' => 'Додаток што им овозможува на корисниците да учествуваат во конкурси (задачи) приредени од администраторите. Жири комисијата има свој посредник за дискутирање и гласање за поднесеното.',
 792+ 'contest-toplink' => 'Мои конкурси',
 793+ 'right-contestadmin' => 'Раководење со конкурси',
 794+ 'right-contestparticipant' => 'Учество во конкурси',
 795+ 'right-contestjudge' => 'Оценување на учесници во конкурс',
 796+ 'group-contestadmin' => 'Конкурсни администратори',
 797+ 'group-contestadmin-member' => '{{GENDER:$1|конкурсен администратор}}',
 798+ 'grouppage-contestadmin' => 'Project:Конкурсни_администратори',
 799+ 'group-contestparticipant' => 'Учесници во конкурси',
 800+ 'group-contestparticipant-member' => '{{GENDER:$1|учесник во конкурси}}',
 801+ 'grouppage-contestparticipant' => 'Project:Учесници_во_конкурси',
 802+ 'group-contestjudge' => 'Конкурсно жири',
 803+ 'group-contestjudge-member' => '{{GENDER:$1|член на конкурсно жири}}',
 804+ 'grouppage-contestjudge' => 'Project:Конкурсно_жири',
 805+ 'prefs-contest' => 'Конкурси',
 806+ 'contest-prefs-showtoplink' => 'Во менито најгоре прикажувај ја врската „[[Special:MyContests|Мои конкурси]]“.',
 807+ 'contest-status-draft' => 'Нацрт (оневозможен)',
 808+ 'contest-status-active' => 'Активен (овозможен)',
 809+ 'contest-status-expired' => 'Истечен (овозможен, рокот изминал)',
 810+ 'contest-status-finished' => 'Завршен (оневозможен)',
 811+ 'special-contest' => 'Погл. на конкурс',
 812+ 'special-contests' => 'Раководење со конкурси',
 813+ 'special-contestsignup' => 'Пријава за конкурс',
 814+ 'special-contestwelcome' => 'Погл. конкурс',
 815+ 'special-editcontest' => 'Уреди конкурс',
 816+ 'special-mycontests' => 'Мои конкурси',
 817+ 'specialpages-group-contest' => 'Конкурси',
 818+ 'contest-nav-contests' => 'Список на конкурси',
 819+ 'contest-nav-editcontest' => 'Уреди конкурс',
 820+ 'contest-nav-contest' => 'Опис и учесници',
 821+ 'contest-nav-contestwelcome' => 'Целна страница',
 822+ 'contest-nav-contestsignup' => 'Страница за пријавување',
 823+ 'contest-special-addnew' => 'Додај нов конкурс',
 824+ 'contest-special-namedoc' => 'Името на конкурсот е назнаката во URL-адресата, т.е. „име“ во Специјална:Конкурс/име',
 825+ 'contest-special-newname' => 'Име на задачата',
 826+ 'contest-special-add' => 'Додај конкурс',
 827+ 'contest-special-existing' => 'Постоечки конкурси',
 828+ 'contest-special-name' => 'Име',
 829+ 'contest-special-status' => 'Статус',
 830+ 'contest-special-submissioncount' => 'Поднесувања',
 831+ 'contest-special-edit' => 'Уреди',
 832+ 'contest-special-delete' => 'Избриши',
 833+ 'contest-special-confirm-delete' => 'Дали сте сигурни дека сакате да го избришете конкурсов?',
 834+ 'contest-special-delete-failed' => 'Не успеав да го избришам конкурсот.',
 835+ 'editcontest-text' => 'Уредувате конкурс.',
 836+ 'editcontest-legend' => 'Конкурс',
 837+ 'contest-edit-name' => 'Име на конкурсот',
 838+ 'contest-edit-status' => 'Статус на конкурсот',
 839+ 'contest-edit-intro' => 'Воведна страница',
 840+ 'contest-edit-opportunities' => 'Страница за можности',
 841+ 'contest-edit-rulespage' => 'Страница со правила',
 842+ 'contest-edit-help' => 'Страница за помош',
 843+ 'contest-edit-signup' => 'Страниз на е-пошта за пријава',
 844+ 'contest-edit-reminder' => 'Страница за потсетник на е-пошта',
 845+ 'contest-edit-end' => 'Крај на конкурсот',
 846+ 'contest-edit-exists-already' => 'Напомена: уредувате веќе постоечки конкурс - не создавате нов.',
 847+ 'contest-edit-submit' => 'Поднеси',
 848+ 'contest-edit-challenges' => 'Задачи на конкурсот',
 849+ 'contest-edit-delete' => 'Избриши задача',
 850+ 'contest-edit-add-first' => 'Додај задача',
 851+ 'contest-edit-add-another' => 'одај друга задача',
 852+ 'contest-edit-confirm-delete' => 'Дали сте сигурни дека сакате да го избришете конкурсов?',
 853+ 'contest-edit-challenge-title' => 'Наслов на задачата',
 854+ 'contest-edit-challenge-text' => 'Текст на задачата',
 855+ 'contest-edit-challenge-oneline' => 'Краток опис',
 856+ 'contest-welcome-unknown' => 'Не постои конкурс со наведеното име.',
 857+ 'contest-welcome-rules' => 'За да учествувате, ќе треба да се согласите со',
 858+ 'contest-welcome-rules-link' => 'правила на конкурсот',
 859+ 'contest-welcome-signup' => 'Пријавете се сега',
 860+ 'contest-welcome-js-off' => 'Корисничкиот посредник за конкурси користи JavaScript за подобрена работа. Или вашиот прелистувач не поддржува JavaScript, или ја имате исклучено.',
 861+ 'contest-welcome-accept-challenge' => 'Задачата е прифатена',
 862+ 'contest-welcome-select-header' => 'Одберете задача:',
 863+ 'contest-signup-unknown' => 'Нема конкурс со наведеното име.',
 864+ 'contest-signup-submit' => 'Пријава',
 865+ 'contest-signup-header' => 'Пополнете го образецот за да се пријавите за $1.',
 866+ 'contest-signup-email' => 'Вашата е-пошта',
 867+ 'contest-signup-realname' => 'Вашето вистинско име',
 868+ 'contest-signup-volunteer' => 'Сакам да бидам доброволец',
 869+ 'contest-signup-wmf' => 'Сакам да работам во Фондацијата Викимедија',
 870+ 'contest-signup-cv' => 'Врска до вашето резиме (CV)',
 871+ 'contest-signup-readrules' => 'Потврдувам дека ги имам прочитано и се согласувам со [[$1|правилата на конкурсот]]',
 872+ 'contest-signup-challenge' => 'Која задача ве бендисува?',
 873+ 'contest-signup-finished' => 'Конкурсот заврши. Ви благодариме за учеството.',
 874+ 'contest-signup-draft' => 'Конкурсот сè уште не е започнат. Бидете трпеливи.',
 875+ 'contest-signup-country' => 'Вашата земја',
 876+ 'contest-signup-require-rules' => 'Ќе треба да се согласите со правилата на конкурсот.',
 877+ 'contest-signup-require-country' => 'Ќе треба да ја наведете земјата кајшто живеете.',
 878+ 'contest-signup-invalid-email' => 'Наведенат е-пошта е неважечка.',
 879+ 'contest-signup-invalid-name' => 'Наведеното име е прекратко.',
 880+ 'contest-signup-require-challenge' => 'Мора да изберете задача.',
 881+ 'contest-signup-invalid-cv' => 'Внесовте неважечка URL-адреса.',
 882+ 'contest-contest-title' => 'Конкурс: $1',
 883+ 'contest-contest-no-results' => 'Нема учесници за прикажување.',
 884+ 'contest-contest-name' => 'Име',
 885+ 'contest-contest-status' => 'Статус',
 886+ 'contest-contest-submissioncount' => 'Бр. на учесници',
 887+ 'contest-contest-contestants' => 'Учесници во конкурсот',
 888+ 'contest-contest-contestants-text' => 'За да оцените некој учесник, стиснете на назнаката во левата колона.',
 889+ 'contest-contestant-id' => 'Назнака',
 890+ 'contest-contestant-challenge-name' => 'Име на задачата',
 891+ 'contest-contestant-volunteer' => 'Доброволец',
 892+ 'contest-contestant-wmf' => 'ФВМ',
 893+ 'contest-contestant-no' => 'не',
 894+ 'contest-contestant-yes' => 'да',
 895+ 'contest-contestant-commentcount' => 'Забелешки',
 896+ 'contest-contestant-overallrating' => 'Оценка',
 897+ 'contest-contestant-rating' => '$1 ($2 {{PLURAL:$2|глас|гласа}})',
 898+ 'contest-contestant-title' => 'Учесник $1 ($2)',
 899+ 'contest-contestant-header-id' => 'Назнака на учесникот',
 900+ 'contest-contestant-header-contest' => 'Име на учесникот',
 901+ 'contest-contestant-header-challenge' => 'Име на задачата',
 902+ 'contest-contestant-header-submission' => 'Врска на поднесеното',
 903+ 'contest-contestant-header-country' => 'Земја на учесникот',
 904+ 'contest-contestant-header-wmf' => 'Заинтересиран за работа во ФВМ',
 905+ 'contest-contestant-header-volunteer' => 'Заинтересиран за доброволни активности',
 906+ 'contest-contestant-header-rating' => 'Оценка',
 907+ 'contest-contestant-header-comments' => 'Бр. на забелешки',
 908+ 'contest-contestant-submission-url' => 'Поднесување',
 909+ 'contest-contestant-notsubmitted' => 'Сè уште не е поднесено',
 910+ 'contest-contestant-comments' => 'Забелешки',
 911+ 'contest-contestant-submit' => 'Зачувај промени',
 912+ 'contest-contestant-comment-by' => 'Забелешка од $1',
 913+ 'contest-contestant-rate' => 'Оценување на учесник',
 914+ 'contest-contestant-not-voted' => 'Сè уште го немате оценето овој учесник.',
 915+ 'contest-contestant-voted' => 'Ваша тековна оценка: $1.',
 916+ 'contest-contestant-permalink' => 'Постојана врска',
 917+ 'contest-email-signup-title' => 'Ви благодариме за учеството во задачата!',
 918+ 'contest-email-reminder-title' => 'Конкурсот трае уште само $1 {{PLURAL:$1|ден|дена}}!',
 919+ 'contest-mycontests-toplink' => 'Мои конкурси',
 920+ 'contest-mycontests-no-contests' => 'Не учествувате во ниеден конкурс.',
 921+ 'contest-mycontests-active-header' => 'Тековни конкурси',
 922+ 'contest-mycontests-finished-header' => 'Минати конкурси',
 923+ 'contest-mycontests-active-text' => 'Моментално учествувате во наведениве конкурси.',
 924+ 'contest-mycontests-finished-text' => 'Ова се конкурсите во кои имате учествувано.',
 925+ 'contest-mycontests-header-contest' => 'Конкурс',
 926+ 'contest-mycontests-header-challenge' => 'Задача',
 927+ 'contest-mycontests-signup-success' => 'Успешно се пријавивте за овој конкурс.',
 928+ 'contest-mycontests-addition-success' => 'Успешно поднесено! Ви благодариме за учеството.',
 929+ 'contest-mycontests-updated-success' => 'Поднесеното е успешно изменето.',
 930+ 'contest-mycontests-sessionfail' => 'Поднесеното не е зачувано поради загуба на сесиски податоци. Обидете се повторно.',
 931+ 'contest-submission-submit' => 'Поднеси',
 932+ 'contest-submission-unknown' => 'Нема конкурс со наведеното име.',
 933+ 'contest-submission-header' => 'Ви благодариме што учествувавте на конкурсот! Штом ќе ја завршите задачата, при поднесувањето подолу можете да ставите и врска.',
 934+ 'contest-submission-finished' => 'Конкурсот заврши. Ви благодариме за учеството!',
 935+ 'contest-submission-submission' => 'Врска до поднесеното',
 936+ 'contest-submission-invalid-url' => 'Оваа URL-адреса не одговара на допуштените формати.',
 937+ 'contest-submission-new-submission' => 'Преостанува да наведете URL-адреса во поднесеното. Ова мора да го сторите пред истекот на рокот.',
 938+ 'contest-submission-current-submission' => 'Ова е URL-адресата на поднесеното, кое можете да го менувате додека не истече рокот.',
 939+ 'contest-submission-domains' => 'Поднесувањето е ограничено на следниве мрежни места: $1',
 940+);
 941+
 942+/** Dutch (Nederlands)
 943+ * @author SPQRobin
 944+ */
 945+$messages['nl'] = array(
 946+ 'contest-toplink' => 'Mijn wedstrijden',
 947+ 'right-contestadmin' => 'Wedstrijden beheren',
 948+ 'right-contestparticipant' => 'Deelnemen aan wedstrijden',
 949+ 'group-contestadmin' => 'Wedstrijdbeheerders',
 950+ 'group-contestadmin-member' => '{{GENDER:$1|wedstrijdbeheerder}}',
 951+ 'grouppage-contestadmin' => 'Project:Wedstrijdbeheerders',
 952+ 'group-contestparticipant' => 'Wedstrijddeelnemers',
 953+ 'group-contestparticipant-member' => '{{GENDER:$1|wedstrijddeelnemer|wedstrijddeelneemster}}',
 954+ 'grouppage-contestparticipant' => 'Project:Wedstrijddeelnemers',
 955+ 'prefs-contest' => 'Wedstrijden',
 956+ 'contest-prefs-showtoplink' => 'Een verwijzing naar [[Special:MyContests|mijn wedstrijden]] weergeven in het bovenste menu.',
 957+ 'contest-status-draft' => 'Ontwerp (uitgeschakeld)',
 958+ 'contest-status-active' => 'Actief (ingeschakeld)',
 959+ 'contest-status-expired' => 'Verlopen (ingeschakeld, voorbij de einddatum)',
 960+ 'contest-status-finished' => 'Gedaan (uitgeschakeld)',
 961+ 'special-contest' => 'Een wedstrijd bekijken',
 962+ 'special-contests' => 'Wedstrijden beheren',
 963+ 'special-contestsignup' => 'Inschrijven voor een wedstrijd',
 964+ 'special-contestwelcome' => 'Een wedstrijd bekijken',
 965+ 'special-editcontest' => 'Een wedstrijd bewerken',
 966+ 'special-mycontests' => 'Mijn wedstrijden',
 967+ 'specialpages-group-contest' => 'Wedstrijden',
 968+ 'contest-nav-contests' => 'Lijst van wedstrijden',
 969+ 'contest-nav-editcontest' => 'Wedstrijd bewerken',
 970+ 'contest-nav-contest' => 'Samenvatting en deelnemers',
 971+ 'contest-special-addnew' => 'Een nieuwe wedstrijd toevoegen',
 972+ 'contest-special-newname' => 'Wedstrijdnaam',
 973+ 'contest-special-add' => 'Wedstrijd toevoegen',
 974+ 'contest-special-existing' => 'Bestaande wedstrijden',
 975+ 'contest-special-name' => 'Naam',
 976+ 'contest-special-status' => 'Status',
 977+ 'contest-special-submissioncount' => 'Aantal inzendingen',
 978+ 'contest-special-edit' => 'Bewerken',
 979+ 'contest-special-delete' => 'Verwijderen',
 980+ 'contest-special-confirm-delete' => 'Weet u zeker dat u deze wedstrijd wilt verwijderen?',
 981+ 'contest-special-delete-failed' => 'Fout bij het verwijderen van de wedstrijd.',
 982+ 'editcontest-text' => 'U bent een wedstrijd aan het bewerken.',
 983+ 'editcontest-legend' => 'Wedstrijd',
 984+ 'contest-edit-name' => 'Wedstrijdnaam',
 985+ 'contest-edit-status' => 'Status van de wedstrijd',
 986+ 'contest-edit-end' => 'Einde van wedstrijd',
 987+ 'contest-edit-exists-already' => 'Opmerking: u bent geen nieuwe wedstrijd aan het maken, maar een bestaande aan het bewerken.',
 988+ 'contest-edit-submit' => 'Opslaan',
 989+ 'contest-edit-challenges' => 'Wedstrijduitdagingen',
 990+ 'contest-edit-delete' => 'Uitdaging verwijderen',
 991+ 'contest-edit-add-first' => 'Een uitdaging toevoegen',
 992+ 'contest-edit-add-another' => 'Een andere uitdaging toevoegen',
 993+ 'contest-edit-confirm-delete' => 'Weet u zeker dat u deze uitdaging wilt verwijderen?',
 994+ 'contest-edit-challenge-title' => 'Uitdagingstitel',
 995+ 'contest-edit-challenge-text' => 'Uitdagingstekst',
 996+ 'contest-edit-challenge-oneline' => 'Korte beschrijving',
 997+ 'contest-welcome-unknown' => 'Er is geen wedstrijd met de opgegeven naam.',
 998+ 'contest-welcome-rules' => 'Om te kunnen deelnemen bent u verplicht akkoord te gaan met',
 999+ 'contest-welcome-rules-link' => 'de wedstrijdregels',
 1000+ 'contest-welcome-signup' => 'Nu inschrijven',
 1001+ 'contest-welcome-accept-challenge' => 'Uitdaging aanvaard',
 1002+ 'contest-welcome-select-header' => 'Selecteer uw uitdaging:',
 1003+ 'contest-signup-unknown' => 'Er is geen wedstrijd met de opgegeven naam.',
 1004+ 'contest-signup-submit' => 'Inschrijven',
 1005+ 'contest-signup-header' => 'Vul het formulier in om uw inschrijving voor $1 te voltooien.',
 1006+ 'contest-signup-email' => 'Uw e-mailadres',
 1007+ 'contest-signup-realname' => 'Uw echte naam',
 1008+ 'contest-signup-cv' => 'Verwijzing naar uw CV',
 1009+ 'contest-signup-readrules' => 'Ik bevestig dat ik [[$1|de wedstrijdregels]] heb gelezen en ermee akkoord ga',
 1010+ 'contest-signup-challenge' => 'Welke uitdaging wilt u aangaan?',
 1011+ 'contest-signup-finished' => 'Deze wedstrijd is afgelopen. Bedankt voor uw deelname!',
 1012+ 'contest-signup-draft' => 'Deze wedstrijd is nog niet begonnen.',
 1013+ 'contest-signup-country' => 'Uw land',
 1014+ 'contest-signup-require-rules' => 'U moet akkoord gaan met de wedstrijdregels.',
 1015+ 'contest-signup-require-country' => 'U moet het land waarin u verblijft opgeven.',
 1016+ 'contest-signup-invalid-email' => 'Het e-mailadres dat u hebt opgegeven is niet geldig.',
 1017+ 'contest-signup-invalid-name' => 'De naam die u hebt opgegeven is te kort.',
 1018+ 'contest-signup-require-challenge' => 'U moet een uitdaging selecteren.',
 1019+ 'contest-signup-invalid-cv' => 'U hebt een ongeldige URL ingevoerd.',
 1020+ 'contest-contest-title' => 'Wedstrijd: $1',
 1021+ 'contest-contest-name' => 'Naam',
 1022+ 'contest-contest-status' => 'Status',
 1023+ 'contest-contest-submissioncount' => 'Aantal deelnemers',
 1024+ 'contest-contestant-challenge-name' => 'Naam van de uitdaging',
 1025+ 'contest-contestant-volunteer' => 'Vrijwilliger',
 1026+ 'contest-contestant-wmf' => 'WMF',
 1027+ 'contest-contestant-no' => 'Nee',
 1028+ 'contest-contestant-yes' => 'Ja',
 1029+ 'contest-contestant-commentcount' => 'Opmerkingen',
 1030+ 'contest-contestant-overallrating' => 'Waardering',
 1031+ 'contest-contestant-rating' => '$1 ($2 {{PLURAL:$2|stem|stemmen}})',
 1032+ 'contest-contestant-header-contest' => 'Wedstrijdnaam',
 1033+ 'contest-contestant-header-challenge' => 'Naam van de uitdaging',
 1034+ 'contest-contestant-header-wmf' => 'Geïnteresseerd in een job bij WMF',
 1035+ 'contest-contestant-header-volunteer' => 'Geïnteresseerd in kansen als vrijwilliger',
 1036+ 'contest-contestant-header-rating' => 'Waardering',
 1037+ 'contest-contestant-header-comments' => 'Aantal opmerkingen',
 1038+ 'contest-contestant-submission-url' => 'Inzending',
 1039+ 'contest-contestant-notsubmitted' => 'Nog niet ingediend',
 1040+ 'contest-contestant-comments' => 'Opmerkingen',
 1041+ 'contest-contestant-submit' => 'Wijzigingen opslaan',
 1042+ 'contest-contestant-comment-by' => 'Opmerking van $1',
 1043+ 'contest-contestant-not-voted' => 'U hebt nog niet gestemd op deze deelnemer.',
 1044+ 'contest-contestant-voted' => 'Uw huidige stem is $1.',
 1045+ 'contest-email-reminder-title' => 'Nog maar $1 {{PLURAL:$1|dag|dagen}} tot het einde van deze uitdaging!',
 1046+ 'contest-mycontests-toplink' => 'Mijn wedstrijden',
 1047+ 'contest-mycontests-no-contests' => 'U neemt niet deel aan een wedstrijd.',
 1048+ 'contest-mycontests-active-header' => 'Lopende wedstrijden',
 1049+ 'contest-mycontests-active-text' => 'Dit zijn de wedstrijden waar u momenteel aan deelneemt.',
 1050+ 'contest-mycontests-header-contest' => 'Wedstrijd',
 1051+ 'contest-mycontests-header-challenge' => 'Uitdaging',
 1052+ 'contest-mycontests-signup-success' => 'U bent ingeschreven voor deze wedstrijd.',
 1053+ 'contest-mycontests-addition-success' => 'Uw inzending is ingediend! Bedankt voor uw deelname aan deze wedstrijd.',
 1054+ 'contest-mycontests-updated-success' => 'Uw inzending is gewijzigd.',
 1055+ 'contest-submission-submit' => 'Opslaan',
 1056+ 'contest-submission-unknown' => 'Er is geen wedstrijd met de opgegeven naam.',
 1057+ 'contest-submission-finished' => 'Deze wedstrijd is afgelopen. Bedankt voor uw deelname!',
 1058+ 'contest-submission-submission' => 'Verwijzing naar uw inzending',
 1059+);
 1060+
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/Contest.i18n.php
___________________________________________________________________
Added: svn:eol-style
11061 + native
Index: branches/wmf/1.18wmf1/extensions/Contest/Contest.hooks.php
@@ -0,0 +1,185 @@
 2+<?php
 3+
 4+/**
 5+ * Static class for hooks handled by the Contest extension.
 6+ *
 7+ * @since 0.1
 8+ *
 9+ * @file Contest.hooks.php
 10+ * @ingroup Contest
 11+ *
 12+ * @licence GNU GPL v3+
 13+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
 14+ */
 15+final class ContestHooks {
 16+
 17+ /**
 18+ * Schema update to set up the needed database tables.
 19+ *
 20+ * @since 0.1
 21+ *
 22+ * @param DatabaseUpdater $updater
 23+ *
 24+ * @return true
 25+ */
 26+ public static function onSchemaUpdate( /* DatabaseUpdater */ $updater = null ) {
 27+ $updater->addExtensionUpdate( array(
 28+ 'addTable',
 29+ 'contests',
 30+ dirname( __FILE__ ) . '/Contest.sql',
 31+ true
 32+ ) );
 33+
 34+ $updater->addExtensionUpdate( array(
 35+ 'addField',
 36+ 'contests',
 37+ 'contest_signup_email',
 38+ dirname( __FILE__ ) . '/sql/AddContestEmailFields.sql',
 39+ true
 40+ ) );
 41+
 42+ return true;
 43+ }
 44+
 45+ /**
 46+ * Hook to add PHPUnit test cases.
 47+ *
 48+ * @since 0.1
 49+ *
 50+ * @param array $files
 51+ *
 52+ * @return true
 53+ */
 54+ public static function registerUnitTests( array &$files ) {
 55+ $testDir = dirname( __FILE__ ) . '/test/';
 56+
 57+ $files[] = $testDir . 'ContestValidationTests.php';
 58+
 59+ return true;
 60+ }
 61+
 62+ /**
 63+ * Called when changing user email address.
 64+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/UserSetEmail
 65+ *
 66+ * Checks if there are any active contests in which the user is participating,
 67+ * and if so, updates the email there as well.
 68+ *
 69+ * @since 0.1
 70+ *
 71+ * @param User $user
 72+ * @param string $email
 73+ *
 74+ * @return true
 75+ */
 76+ public static function onUserSetEmail( User $user, &$email ) {
 77+ $dbr = wfGetDB( DB_SLAVE );
 78+
 79+ $contestants = $dbr->select(
 80+ array( 'contest_contestants', 'contests' ),
 81+ array( 'contestant_id' ),
 82+ array( 'contest_status' => Contest::STATUS_ACTIVE, 'contestant_user_id' => $user->getId() ),
 83+ __METHOD__,
 84+ array(),
 85+ array( 'contests' => array( 'INNER JOIN', array( 'contest_id=contestant_contest_id' ) ) )
 86+ );
 87+
 88+ $contestantIds = array();
 89+
 90+ foreach ( $contestants as $contestant ) {
 91+ $contestantIds[] = $contestant->contestant_id;
 92+ }
 93+
 94+ if ( count( $contestantIds ) > 0 ) {
 95+ ContestContestant::s()->update(
 96+ array( 'email' => $email ),
 97+ array( 'id' => $contestantIds )
 98+ );
 99+ }
 100+
 101+ return true;
 102+ }
 103+
 104+ /**
 105+ * Called after the personal URLs have been set up, before they are shown.
 106+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/PersonalUrls
 107+ *
 108+ * @since 0.1
 109+ *
 110+ * @param array $personal_urls
 111+ * @param Title $title
 112+ *
 113+ * @return true
 114+ */
 115+ public static function onPersonalUrls( array &$personal_urls, Title &$title ) {
 116+ if ( ContestSettings::get( 'enableTopLink' ) ) {
 117+ global $wgUser;
 118+
 119+ // Find the watchlist item and replace it by the my contests link and itself.
 120+ if ( $wgUser->isLoggedIn() && $wgUser->getOption( 'contest_showtoplink' ) ) {
 121+ $keys = array_keys( $personal_urls );
 122+ $watchListLocation = array_search( 'watchlist', $keys );
 123+ $watchListItem = $personal_urls[$keys[$watchListLocation]];
 124+
 125+ $url = SpecialPage::getTitleFor( 'MyContests' )->getLinkUrl();
 126+ $myContests = array(
 127+ 'text' => wfMsg( 'contest-toplink' ),
 128+ 'href' => $url,
 129+ 'active' => ( $url == $title->getLinkUrl() )
 130+ );
 131+
 132+ array_splice( $personal_urls, $watchListLocation, 1, array( $myContests, $watchListItem ) );
 133+ }
 134+ }
 135+
 136+ return true;
 137+ }
 138+
 139+ /**
 140+ * Adds the preferences of Contest to the list of available ones.
 141+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/GetPreferences
 142+ *
 143+ * @since 0.1
 144+ *
 145+ * @param User $user
 146+ * @param array $preferences
 147+ *
 148+ * @return true
 149+ */
 150+ public static function onGetPreferences( User $user, array &$preferences ) {
 151+ if ( ContestSettings::get( 'enableTopLink' ) ) {
 152+ $preferences['contest_showtoplink'] = array(
 153+ 'type' => 'toggle',
 154+ 'label-message' => 'contest-prefs-showtoplink',
 155+ 'section' => 'contest',
 156+ );
 157+ }
 158+
 159+ return true;
 160+ }
 161+
 162+ /**
 163+ * Used when generating internal and interwiki links in Linker::link(),
 164+ * just before the function returns a value.
 165+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/LinkEnd
 166+ *
 167+ * @since 0.1
 168+ *
 169+ * @param $skin
 170+ * @param Title $target
 171+ * @param array $options
 172+ * @param string $text
 173+ * @param array $attribs
 174+ * @param $ret
 175+ *
 176+ * @return true
 177+ */
 178+ public static function onLinkEnd( $skin, Title $target, array $options, &$text, array &$attribs, &$ret ) {
 179+ if ( $GLOBALS['wgContestEmailParse'] ) {
 180+ $attribs['href'] = $target->getFullURL();
 181+ }
 182+
 183+ return true;
 184+ }
 185+
 186+}
Property changes on: branches/wmf/1.18wmf1/extensions/Contest/Contest.hooks.php
___________________________________________________________________
Added: svn:eol-style
1187 + native

Status & tagging log