r57734 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r57733‎ | r57734 | r57735 >
Date:00:44, 15 October 2009
Author:tstarling
Status:deferred
Tags:
Comment:
* Added radio-range and approval ballots
* Added test files
* Added ballot error location highlighting feature. Needs some work to make it look prettier.
* Moved some boilerplate from submitForm() in the ballot subclasses into to the parent, the children now implement submitQuestion() instead.
* Renamed $options to $params in some places to avoid confusion with SecurePoll_Option
* In SecurePoll_Context: clear caches when the store class changes.
* A couple of new images from the Crystal project
Modified paths:
  • /trunk/extensions/SecurePoll/SecurePoll.i18n.php (modified) (history)
  • /trunk/extensions/SecurePoll/SecurePoll.php (modified) (history)
  • /trunk/extensions/SecurePoll/SecurePoll.sql (modified) (history)
  • /trunk/extensions/SecurePoll/includes/ballots/ApprovalBallot.php (added) (history)
  • /trunk/extensions/SecurePoll/includes/ballots/Ballot.php (modified) (history)
  • /trunk/extensions/SecurePoll/includes/ballots/ChooseBallot.php (modified) (history)
  • /trunk/extensions/SecurePoll/includes/ballots/PreferentialBallot.php (modified) (history)
  • /trunk/extensions/SecurePoll/includes/ballots/RadioRangeBallot.php (added) (history)
  • /trunk/extensions/SecurePoll/includes/entities/Election.php (modified) (history)
  • /trunk/extensions/SecurePoll/includes/entities/Entity.php (modified) (history)
  • /trunk/extensions/SecurePoll/includes/entities/Question.php (modified) (history)
  • /trunk/extensions/SecurePoll/includes/main/Context.php (modified) (history)
  • /trunk/extensions/SecurePoll/includes/pages/VotePage.php (modified) (history)
  • /trunk/extensions/SecurePoll/includes/talliers/Tallier.php (modified) (history)
  • /trunk/extensions/SecurePoll/resources/SecurePoll.css (modified) (history)
  • /trunk/extensions/SecurePoll/resources/SecurePoll.js (modified) (history)
  • /trunk/extensions/SecurePoll/resources/down-16.png (added) (history)
  • /trunk/extensions/SecurePoll/resources/warning-22.png (added) (history)
  • /trunk/extensions/SecurePoll/test (added) (history)
  • /trunk/extensions/SecurePoll/test/3way-test.xml (added) (history)
  • /trunk/extensions/SecurePoll/test/approval-test.xml (added) (history)
  • /trunk/extensions/SecurePoll/test/radio-range.xml (added) (history)
  • /trunk/extensions/SecurePoll/test/schulze-test.xml (added) (history)

Diff [purge]

Index: trunk/extensions/SecurePoll/test/3way-test.xml
@@ -0,0 +1,50 @@
 2+<SecurePoll>
 3+<election>
 4+<configuration>
 5+<title>Radio range test 1</title>
 6+<ballot>radio-range</ballot>
 7+<tally>histogram-range</tally>
 8+<primaryLang>en</primaryLang>
 9+<startDate>2009-07-19T00:00:00Z</startDate>
 10+<endDate>2019-08-10T00:00:00Z</endDate>
 11+<id>50</id>
 12+<property name="admins">Tim</property>
 13+<message name="title" lang="en">RR1 test</message>
 14+<message name="intro" lang="en">Click the buttons</message>
 15+<message name="jump-text" lang="en">RR1 test jump text</message>
 16+<message name="return-text" lang="en">RR1 test return text</message>
 17+<auth>local</auth>
 18+<question>
 19+<property name="min-score">-1</property>
 20+<property name="max-score">1</property>
 21+<property name="column-label-msgs">yes</property>
 22+<property name="default-score">0</property>
 23+<message name="column-1" lang="en">Oppose</message>
 24+<message name="column0" lang="en">Abstain</message>
 25+<message name="column+1" lang="en">Support</message>
 26+<id>51</id>
 27+<message name="text" lang="en">RR1 test question</message>
 28+<option>
 29+<id>52</id>
 30+<message name="text" lang="en">AAA</message>
 31+<message name="text" lang="fr">AAA fr</message>
 32+</option>
 33+<option>
 34+<id>53</id>
 35+<message name="text" lang="en">B</message>
 36+<message name="text" lang="fr">B fr</message>
 37+</option>
 38+<option>
 39+<id>54</id>
 40+<message name="text" lang="en">C</message>
 41+<message name="text" lang="fr">C fr</message>
 42+</option>
 43+<option>
 44+<id>55</id>
 45+<message name="text" lang="en">D</message>
 46+<message name="text" lang="fr">D fr</message>
 47+</option>
 48+</question>
 49+</configuration>
 50+</election>
 51+</SecurePoll>
Index: trunk/extensions/SecurePoll/test/approval-test.xml
@@ -0,0 +1,43 @@
 2+<SecurePoll>
 3+<election>
 4+<configuration>
 5+<title>Approval test</title>
 6+<ballot>approval</ballot>
 7+<tally>range</tally>
 8+<primaryLang>en</primaryLang>
 9+<startDate>2009-07-19T00:00:00Z</startDate>
 10+<endDate>2019-08-10T00:00:00Z</endDate>
 11+<id>40</id>
 12+<property name="admins">Tim</property>
 13+<message name="title" lang="en">Approval test</message>
 14+<message name="intro" lang="en">Approval test intro</message>
 15+<message name="jump-text" lang="en">Approval test jump text</message>
 16+<message name="return-text" lang="en">Approval test return text</message>
 17+<auth>local</auth>
 18+<question>
 19+<id>41</id>
 20+<message name="text" lang="en">Approval test question</message>
 21+<option>
 22+<id>42</id>
 23+<message name="text" lang="en">AAAA</message>
 24+<message name="text" lang="fr">AAAA fr</message>
 25+</option>
 26+<option>
 27+<id>43</id>
 28+<message name="text" lang="en">B</message>
 29+<message name="text" lang="fr">B fr</message>
 30+</option>
 31+<option>
 32+<id>44</id>
 33+<message name="text" lang="en">C</message>
 34+<message name="text" lang="fr">C fr</message>
 35+</option>
 36+<option>
 37+<id>45</id>
 38+<message name="text" lang="en">D</message>
 39+<message name="text" lang="fr">D fr</message>
 40+</option>
 41+</question>
 42+</configuration>
 43+</election>
 44+</SecurePoll>
Index: trunk/extensions/SecurePoll/test/radio-range.xml
@@ -0,0 +1,38 @@
 2+<SecurePoll>
 3+<election>
 4+<configuration>
 5+<title>Radio range test 2</title>
 6+<ballot>radio-range</ballot>
 7+<tally>histogram-range</tally>
 8+<primaryLang>en</primaryLang>
 9+<startDate>2009-07-19T00:00:00Z</startDate>
 10+<endDate>2019-08-10T00:00:00Z</endDate>
 11+<id>60</id>
 12+<property name="admins">Tim</property>
 13+<property name="must-answer-all">yes</property>
 14+<message name="title" lang="en">RR2 test</message>
 15+<message name="intro" lang="en">RR2 test intro</message>
 16+<message name="jump-text" lang="en">RR2 test jump text</message>
 17+<message name="return-text" lang="en">RR2 test return text</message>
 18+<auth>local</auth>
 19+<question>
 20+<property name="min-score">1</property>
 21+<property name="max-score">5</property>
 22+<id>61</id>
 23+<message name="text" lang="en">Indicate how much you agree with the following statements by choosing a number from 1 to 5, where 1=strongly disagree, 5=strongly agree.</message>
 24+<option>
 25+<id>62</id>
 26+<message name="text" lang="en">I like icecream.</message>
 27+</option>
 28+<option>
 29+<id>63</id>
 30+<message name="text" lang="en">Kittens are evil.</message>
 31+</option>
 32+<option>
 33+<id>64</id>
 34+<message name="text" lang="en">Radiative forcing due to a doubling of atmospheric CO&lt;sub&gt;2&lt;/sub&gt; is 3.7 W/m&lt;sup&gt;2&lt;/sup&gt;.</message>
 35+</option>
 36+</question>
 37+</configuration>
 38+</election>
 39+</SecurePoll>
Index: trunk/extensions/SecurePoll/test/schulze-test.xml
@@ -0,0 +1,43 @@
 2+<SecurePoll>
 3+<election>
 4+<configuration>
 5+<title>Schulze test</title>
 6+<ballot>preferential</ballot>
 7+<tally>schulze</tally>
 8+<primaryLang>en</primaryLang>
 9+<startDate>2009-07-19T00:00:00Z</startDate>
 10+<endDate>2019-08-10T00:00:00Z</endDate>
 11+<id>11</id>
 12+<property name="admins">Tim</property>
 13+<message name="title" lang="en">Schulze test</message>
 14+<message name="intro" lang="en">Schulze test intro</message>
 15+<message name="jump-text" lang="en">Schulze test jump text</message>
 16+<message name="return-text" lang="en">Schulze test return text</message>
 17+<auth>local</auth>
 18+<question>
 19+<id>12</id>
 20+<message name="text" lang="en">Schulze test question</message>
 21+<option>
 22+<id>13</id>
 23+<message name="text" lang="en">A</message>
 24+<message name="text" lang="fr">A fr</message>
 25+</option>
 26+<option>
 27+<id>14</id>
 28+<message name="text" lang="en">B</message>
 29+<message name="text" lang="fr">B fr</message>
 30+</option>
 31+<option>
 32+<id>15</id>
 33+<message name="text" lang="en">C</message>
 34+<message name="text" lang="fr">C fr</message>
 35+</option>
 36+<option>
 37+<id>16</id>
 38+<message name="text" lang="en">D</message>
 39+<message name="text" lang="fr">D fr</message>
 40+</option>
 41+</question>
 42+</configuration>
 43+</election>
 44+</SecurePoll>
Index: trunk/extensions/SecurePoll/SecurePoll.i18n.php
@@ -62,6 +62,8 @@
6363 'securepoll-invalid-rank' => 'Invalid rank. You must give candidates a rank between 1 and 999.',
6464 'securepoll-unranked-options' => 'Some options were not ranked.
6565 You must give all options a rank between 1 and 999.',
 66+ 'securepoll-invalid-score' => 'The score must be a number between $1 and $2.',
 67+ 'securepoll-unanswered-options' => 'You must give a response for every question.',
6668
6769 # Authorisation related
6870 'securepoll-remote-auth-error' => 'Error fetching your account information from the server.',
Index: trunk/extensions/SecurePoll/SecurePoll.php
@@ -49,9 +49,11 @@
5050
5151 $wgAutoloadClasses = $wgAutoloadClasses + array(
5252 # ballots
 53+ 'SecurePoll_ApprovalBallot' => "$dir/includes/ballots/ApprovalBallot.php",
5354 'SecurePoll_Ballot' => "$dir/includes/ballots/Ballot.php",
5455 'SecurePoll_ChooseBallot' => "$dir/includes/ballots/ChooseBallot.php",
5556 'SecurePoll_PreferentialBallot' => "$dir/includes/ballots/PreferentialBallot.php",
 57+ 'SecurePoll_RadioRangeBallot' => "$dir/includes/ballots/RadioRangeBallot.php",
5658
5759 # crypt
5860 'SecurePoll_Crypt' => "$dir/includes/crypt/Crypt.php",
Index: trunk/extensions/SecurePoll/includes/pages/VotePage.php
@@ -97,7 +97,7 @@
9898 /**
9999 * Show the voting form.
100100 */
101 - function showForm() {
 101+ function showForm( $status = false ) {
102102 global $wgOut;
103103
104104 // Show introduction
@@ -114,7 +114,8 @@
115115
116116 $wgOut->addHTML(
117117 "<form name=\"securepoll\" id=\"securepoll\" method=\"post\" action=\"$encAction\">\n" .
118 - $this->election->getBallot()->getForm() .
 118+ $this->election->getBallot()->getForm( $status ) .
 119+ "<br/>\n" .
119120 "<input name=\"submit\" type=\"submit\" value=\"$encOK\">\n" .
120121 "<input type='hidden' name='edit_token' value=\"{$encToken}\" /></td>\n" .
121122 "</form>"
@@ -130,10 +131,7 @@
131132 $ballot = $this->election->getBallot();
132133 $status = $ballot->submitForm();
133134 if ( !$status->isOK() ) {
134 - $wgOut->addWikiText( '<div class="securepoll-error-box">' .
135 - $status->getWikiText( 'securepoll-bad-ballot-submission' ) .
136 - '</div>' );
137 - $this->showForm();
 135+ $this->showForm( $status );
138136 } else {
139137 $this->logVote( $status->value );
140138 }
Index: trunk/extensions/SecurePoll/includes/talliers/Tallier.php
@@ -34,7 +34,7 @@
3535 }
3636
3737 function convertRanksToHtml( $ranks ) {
38 - $s = "<table class=\"securepoll-results\">";
 38+ $s = "<table class=\"securepoll-table\">";
3939 $ids = array_keys( $ranks );
4040 foreach ( $ids as $i => $oid ) {
4141 $rank = $ranks[$oid];
Index: trunk/extensions/SecurePoll/includes/ballots/RadioRangeBallot.php
@@ -0,0 +1,213 @@
 2+<?php
 3+
 4+/**
 5+ * A ballot form for range voting where the number of allowed responses is small,
 6+ * allowing a radio button table interface and histogram tallying.
 7+ *
 8+ * Election properties:
 9+ * must-answer-all
 10+ *
 11+ * Question properties:
 12+ * min-score
 13+ * max-score
 14+ * column-label-msgs
 15+ *
 16+ * Question messages:
 17+ * column-1, column0, column+1, etc.
 18+ */
 19+class SecurePoll_RadioRangeBallot extends SecurePoll_Ballot {
 20+ var $columnLabels, $minMax;
 21+
 22+ function getTallyTypes() {
 23+ return array( 'plurality', 'histogram-range' );
 24+ }
 25+
 26+ function getMinMax( $question ) {
 27+ $min = intval( $question->getProperty( 'min-score' ) );
 28+ $max = intval( $question->getProperty( 'max-score' ) );
 29+ if ( $max <= $min ) {
 30+ throw new MWException( __METHOD__.': min/max not configured' );
 31+ }
 32+ return array( $min, $max );
 33+ }
 34+
 35+ function getColumnLabels( $question ) {
 36+ list( $min, $max ) = $this->getMinMax( $question );
 37+ $labels = array();
 38+ $useMessageLabels = $question->getProperty( 'column-label-msgs' );
 39+ if ( $useMessageLabels ) {
 40+ for ( $score = $min; $score <= $max; $score++ ) {
 41+ $signedScore = $this->addSign( $question, $score );
 42+ $labels[$score] = $question->getMessage( "column$signedScore" );
 43+ }
 44+ } else {
 45+ global $wgLang;
 46+ for ( $score = $min; $score <= $max; $score++ ) {
 47+ $labels[$score] = $wgLang->formatNum( $score );
 48+ }
 49+ }
 50+ return $labels;
 51+ }
 52+
 53+ function getMessageNames( $entity ) {
 54+ if ( $entity->getType() !== 'question' ) {
 55+ return array();
 56+ }
 57+ if ( !$entity->getProperty( 'column-label-msgs' ) ) {
 58+ return array();
 59+ }
 60+ $msgs = array();
 61+ list( $min, $max ) = $this->getMinMax( $entity );
 62+ for ( $score = min; $score <= $max; $score++ ) {
 63+ $signedScore = $this->addSign( $entity, $score );
 64+ $msgs[] = "column$signedScore";
 65+ }
 66+ return $msgs;
 67+ }
 68+
 69+ function addSign( $question, $score ) {
 70+ list( $min, $max ) = $this->getMinMax( $question );
 71+ if ( $min < 0 && $score > 0 ) {
 72+ return "+$score";
 73+ } else {
 74+ return $score;
 75+ }
 76+ }
 77+
 78+ function getQuestionForm( $question, $options ) {
 79+ global $wgRequest;
 80+ $name = 'securepoll_q' . $question->getId();
 81+ list( $min, $max ) = $this->getMinMax( $question );
 82+ $labels = $this->getColumnLabels( $question );
 83+
 84+ $s = "<table class=\"securepoll-ballot-table\">\n" .
 85+ "<tr>\n" .
 86+ "<th>&nbsp;</th>\n";
 87+ foreach ( $labels as $label ) {
 88+ $s .= Xml::element( 'th', array(), $label ) . "\n";
 89+ }
 90+ $defaultScore = $question->getProperty( 'default-score' );
 91+
 92+ foreach ( $options as $option ) {
 93+ $optionHTML = $option->parseMessageInline( 'text' );
 94+ $optionId = $option->getId();
 95+ $inputId = "{$name}_opt{$optionId}";
 96+ $oldValue = $wgRequest->getVal( $inputId, $defaultScore );
 97+ $s .= "<tr class=\"securepoll-ballot-row\">\n" .
 98+ Xml::tags( 'td',
 99+ array( 'class' => 'securepoll-ballot-optlabel' ),
 100+ $this->errorLocationIndicator( $inputId ) . $optionHTML
 101+ );
 102+
 103+ foreach ( $labels as $score => $label ) {
 104+ $s .=
 105+ Xml::tags( 'td', array(),
 106+ Xml::radio( $inputId, $score, !strcmp( $oldValue, $score ),
 107+ array( 'title' => $label ) )
 108+ ) . "\n";
 109+ }
 110+ $s .= "</tr>\n";
 111+ }
 112+ $s .= "</table>\n";
 113+ return $s;
 114+ }
 115+
 116+ function submitQuestion( $question, $status ) {
 117+ global $wgRequest, $wgLang;
 118+
 119+ $options = $question->getOptions();
 120+ $record = '';
 121+ $ok = true;
 122+ list( $min, $max ) = $this->getMinMax( $question );
 123+ $defaultScore = $question->getProperty( 'default-score' );
 124+ foreach ( $options as $option ) {
 125+ $id = 'securepoll_q' . $question->getId() . '_opt' . $option->getId();
 126+ $score = $wgRequest->getVal( $id );
 127+
 128+ if ( is_numeric( $score ) ) {
 129+ if ( $score < $min || $score > $max ) {
 130+ $status->sp_fatal( 'securepoll-invalid-score', $id,
 131+ $wgLang->formatNum( $min ), $wgLang->formatNum( $max ) );
 132+ $ok = false;
 133+ continue;
 134+ } else {
 135+ $score = intval( $score );
 136+ }
 137+ } elseif ( strval( $score ) === '' ) {
 138+ if ( $this->election->getProperty( 'must-answer-all' ) ) {
 139+ $status->sp_fatal( 'securepoll-unanswered-options', $id );
 140+ $ok = false;
 141+ continue;
 142+ } else {
 143+ $score = $defaultScore;
 144+ }
 145+ } else {
 146+ $status->sp_fatal( 'securepoll-invalid-score', $id,
 147+ $wgLang->formatNum( $min ), $wgLang->formatNum( $max ) );
 148+ $ok = false;
 149+ continue;
 150+ }
 151+ $record .= sprintf( 'Q%08X-A%08X-S%+011d--',
 152+ $question->getId(), $option->getId(), $score );
 153+ }
 154+ if ( $ok ) {
 155+ return $record;
 156+ }
 157+ }
 158+
 159+ function unpackRecord( $record ) {
 160+ $scores = array();
 161+ $itemLength = 8 + 8 + 11 + 7;
 162+ $questions = array();
 163+ foreach ( $this->election->getQuestions() as $question ) {
 164+ $questions[$question->getId()] = $question;
 165+ }
 166+ for ( $offset = 0; $offset < strlen( $record ); $offset += $itemLength ) {
 167+ if ( !preg_match( '/Q([0-9A-F]{8})-A([0-9A-F]{8})-S([+-][0-9]{10})--/A',
 168+ $record, $m, 0, $offset ) )
 169+ {
 170+ wfDebug( __METHOD__.": regex doesn't match\n" );
 171+ return false;
 172+ }
 173+ $qid = intval( base_convert( $m[1], 16, 10 ) );
 174+ $oid = intval( base_convert( $m[2], 16, 10 ) );
 175+ $score = intval( $m[3] );
 176+ if ( !isset( $questions[$qid] ) ) {
 177+ wfDebug( __METHOD__.": invalid question ID\n" );
 178+ return false;
 179+ }
 180+ list( $min, $max ) = $this->getMinMax( $questions[$qid] );
 181+ if ( $score < $min || $score > $max ) {
 182+ wfDebug( __METHOD__.": score out of range\n" );
 183+ }
 184+ $scores[$qid][$oid] = $score;
 185+ }
 186+ return $scores;
 187+ }
 188+
 189+ function convertScores( $scores, $params = array() ) {
 190+ $result = array();
 191+ foreach ( $this->election->getQuestions() as $question ) {
 192+ $qid = $question->getId();
 193+ if ( !isset( $scores[$qid] ) ) {
 194+ return false;
 195+ }
 196+ $s = '';
 197+ $qscores = $scores[$qid];
 198+ ksort( $qscores );
 199+ $first = true;
 200+ foreach ( $qscores as $score ) {
 201+ if ( $first ) {
 202+ $first = false;
 203+ } else {
 204+ $s .= ', ';
 205+ }
 206+ $s .= $score;
 207+ }
 208+ $result[$qid] = $s;
 209+ }
 210+ return $result;
 211+ }
 212+}
 213+
 214+
Property changes on: trunk/extensions/SecurePoll/includes/ballots/RadioRangeBallot.php
___________________________________________________________________
Name: svn:eol-style
1215 + native
Index: trunk/extensions/SecurePoll/includes/ballots/Ballot.php
@@ -16,9 +16,11 @@
1717 /**
1818 * Get the HTML form segment for a single question
1919 * @param $question SecurePoll_Question
 20+ * @param $options Array of options, in the order they should be displayed
 21+ * @param $prevStatus Status of previous form submission
2022 * @return string
2123 */
22 - abstract function getQuestionForm( $question );
 24+ abstract function getQuestionForm( $question, $options );
2325
2426 /**
2527 * Called when the form is submitted. This returns a Status object which,
@@ -26,9 +28,32 @@
2729 * preserve voter privacy, voting records should be the same length
2830 * regardless of voter choices.
2931 */
30 - abstract function submitForm();
 32+ function submitForm() {
 33+ $questions = $this->election->getQuestions();
 34+ $record = '';
 35+ $status = new SecurePoll_BallotStatus( $this->context );
3136
 37+ foreach ( $questions as $question ) {
 38+ $record .= $this->submitQuestion( $question, $status );
 39+ }
 40+ if ( $status->isOK() ) {
 41+ $status->value = $record . "\n";
 42+ }
 43+ return $status;
 44+ }
 45+
3246 /**
 47+ * Construct a string record for a given question, during form submission.
 48+ *
 49+ * If there is a problem with the form data, the function should set a
 50+ * fatal error in the $status object and return null.
 51+ *
 52+ * @param Status
 53+ * @return string
 54+ */
 55+ abstract function submitQuestion( $question, $status );
 56+
 57+ /**
3358 * Unpack a string record into an array format suitable for the tally type
3459 */
3560 abstract function unpackRecord( $record );
@@ -60,6 +85,8 @@
6186 return new SecurePoll_PreferentialBallot( $context, $election );
6287 case 'choose':
6388 return new SecurePoll_ChooseBallot( $context, $election );
 89+ case 'radio-range':
 90+ return new SecurePoll_RadioRangeBallot( $context, $election );
6491 default:
6592 throw new MWException( "Invalid ballot type: $type" );
6693 }
@@ -80,21 +107,128 @@
81108 * they will be added by the VotePage.
82109 * @return string
83110 */
84 - function getForm() {
 111+ function getForm( $prevStatus = false ) {
85112 global $wgParser, $wgTitle;
86113 $questions = $this->election->getQuestions();
87114 if ( $this->election->getProperty( 'shuffle-questions' ) ) {
88115 shuffle( $questions );
89116 }
 117+ $shuffleOptions = $this->election->getProperty( 'shuffle-options' );
 118+ $this->setErrorStatus( $prevStatus );
90119
91120 $s = '';
92121 foreach ( $questions as $question ) {
 122+ $options = $question->getOptions();
 123+ if ( $shuffleOptions ) {
 124+ shuffle( $options );
 125+ }
93126 $s .= "<hr/>\n" .
94127 $question->parseMessage( 'text' ) .
95 - $this->getQuestionForm( $question ) .
 128+ $this->getQuestionForm( $question, $options ) .
96129 "\n";
97130 }
 131+ if ( $prevStatus ) {
 132+ $s = $this->formatStatus( $prevStatus ) . $s;
 133+ }
98134 return $s;
99135 }
 136+
 137+ function setErrorStatus( $status ) {
 138+ if ( $status ) {
 139+ $this->prevErrorIds = $status->sp_getIds();
 140+ $this->prevStatus = $status;
 141+ } else {
 142+ $this->prevErrorIds = array();
 143+ }
 144+ $this->usedErrorIds = array();
 145+ }
 146+
 147+ function errorLocationIndicator( $id ) {
 148+ if ( !isset( $this->prevErrorIds[$id] ) ) {
 149+ return '';
 150+ }
 151+ $this->usedErrorIds[$id] = true;
 152+ return
 153+ Xml::element( 'img', array(
 154+ 'src' => $this->context->getResourceUrl( 'warning-22.png' ),
 155+ 'width' => 22,
 156+ 'height' => 22,
 157+ 'id' => "$id-location",
 158+ 'class' => 'securepoll-error-location',
 159+ 'alt' => '',
 160+ 'title' => $this->prevStatus->sp_getMessageText( $id )
 161+ ) );
 162+ }
 163+
 164+ /**
 165+ * Convert a SecurePoll_BallotStatus object to HTML
 166+ */
 167+ function formatStatus( $status ) {
 168+ return $status->sp_getHTML( $this->usedErrorIds );
 169+ }
100170 }
101171
 172+class SecurePoll_BallotStatus extends Status {
 173+ var $sp_context;
 174+ var $sp_ids = array();
 175+
 176+ function __construct( $context ) {
 177+ $this->sp_context = $context;
 178+ }
 179+
 180+ function sp_fatal( $message, $id /*, parameters... */ ) {
 181+ $params = array_slice( func_get_args(), 2 );
 182+ $this->errors[] = array(
 183+ 'type' => 'error',
 184+ 'securepoll-id' => $id,
 185+ 'message' => $message,
 186+ 'params' => $params );
 187+ $this->sp_ids[$id] = true;
 188+ $this->ok = false;
 189+ }
 190+
 191+ function sp_getIds() {
 192+ return $this->sp_ids;
 193+ }
 194+
 195+ function sp_getHTML( $usedIds ) {
 196+ if ( !$this->errors ) {
 197+ return '';
 198+ }
 199+ $s = '<ul class="securepoll-error-box">';
 200+ foreach ( $this->errors as $error ) {
 201+ $text = wfMsgReal( $error['message'], $error['params'] );
 202+ if ( isset( $error['securepoll-id'] ) ) {
 203+ $id = $error['securepoll-id'];
 204+ if ( isset( $usedIds[$id] ) ) {
 205+ $s .= '<li>' .
 206+ Xml::openElement( 'a', array(
 207+ 'href' => '#' . urlencode( "$id-location" ),
 208+ 'class' => 'securepoll-error-jump'
 209+ ) ) .
 210+ Xml::element( 'img', array(
 211+ 'alt' => '',
 212+ 'src' => $this->sp_context->getResourceUrl( 'down-16.png' ),
 213+ ) ) .
 214+ '</a>' .
 215+ htmlspecialchars( $text ) .
 216+ "</li>\n";
 217+ continue;
 218+ }
 219+ }
 220+ $s .= '<li>' . htmlspecialchars( $text ) . "</li>\n";
 221+ }
 222+ $s .= "</ul>\n";
 223+ $s .= '<script type="text/javascript"> securepoll_ballot_setup(); </script>';
 224+ return $s;
 225+ }
 226+
 227+ function sp_getMessageText( $id ) {
 228+ foreach ( $this->errors as $error ) {
 229+ if ( $error['securepoll-id'] !== $id ) {
 230+ continue;
 231+ }
 232+ return wfMsgReal( $error['message'], $error['params'] );
 233+ }
 234+ }
 235+}
Index: trunk/extensions/SecurePoll/includes/ballots/ChooseBallot.php
@@ -3,10 +3,6 @@
44 /**
55 * A ballot class which asks the user to choose one answer only from the
66 * given options, for each question.
7 - *
8 - * The following election properties are used:
9 - * shuffle-questions when present and true, the questions are shown in random order
10 - * shuffle-options when present and true, the options are shown in random order
117 */
128 class SecurePoll_ChooseBallot extends SecurePoll_Ballot {
139 /**
@@ -21,13 +17,10 @@
2218 /**
2319 * Get the HTML form segment for a single question
2420 * @param $question SecurePoll_Question
 21+ * @param $options Array of options, in the order they should be displayed
2522 * @return string
2623 */
27 - function getQuestionForm( $question ) {
28 - $options = $question->getChildren();
29 - if ( $this->election->getProperty( 'shuffle-options' ) ) {
30 - shuffle( $options );
31 - }
 24+ function getQuestionForm( $question, $options ) {
3225 $name = 'securepoll_q' . $question->getId();
3326 $s = '';
3427 foreach ( $options as $option ) {
@@ -44,23 +37,14 @@
4538 return $s;
4639 }
4740
48 - /**
49 - * Called when the form is submitted.
50 - * @return Status
51 - */
52 - function submitForm() {
 41+ function submitQuestion( $question, $status ) {
5342 global $wgRequest;
54 - $questions = $this->election->getQuestions();
55 - $record = '';
56 - foreach ( $questions as $question ) {
57 - $result = $wgRequest->getInt( 'securepoll_q' . $question->getId() );
58 - if ( !$result ) {
59 - return Status::newFatal( 'securepoll-unanswered-questions' );
60 - }
61 - $record .= $this->packRecord( $question->getId(), $result );
 43+ $result = $wgRequest->getInt( 'securepoll_q' . $question->getId() );
 44+ if ( !$result ) {
 45+ $status->fatal( 'securepoll-unanswered-questions' );
 46+ } else {
 47+ return $this->packRecord( $question->getId(), $result );
6248 }
63 - $record .= "\n";
64 - return Status::newGood( $record );
6549 }
6650
6751 function packRecord( $qid, $oid ) {
@@ -82,7 +66,7 @@
8367 return $result;
8468 }
8569
86 - function convertScores( $scores, $options = array() ) {
 70+ function convertScores( $scores, $params = array() ) {
8771 $s = '';
8872 foreach ( $this->election->getQuestions() as $question ) {
8973 $qid = $question->getId();
@@ -92,7 +76,7 @@
9377 if ( $s !== '' ) {
9478 $s .= '; ';
9579 }
96 - $oid = keys( $scores );
 80+ $oid = key( $scores );
9781 $option = $this->election->getOption( $oid );
9882 $s .= $option->getMessage( 'name' );
9983 }
Index: trunk/extensions/SecurePoll/includes/ballots/PreferentialBallot.php
@@ -12,12 +12,8 @@
1313 return array( 'schulze' );
1414 }
1515
16 - function getQuestionForm( $question ) {
 16+ function getQuestionForm( $question, $options ) {
1717 global $wgRequest;
18 - $options = $question->getChildren();
19 - if ( $this->election->getProperty( 'shuffle-options' ) ) {
20 - shuffle( $options );
21 - }
2218 $name = 'securepoll_q' . $question->getId();
2319 $s = '';
2420 foreach ( $options as $option ) {
@@ -32,6 +28,7 @@
3329 'maxlength' => 3,
3430 ) ) .
3531 '&nbsp;' .
 32+ $this->errorLocationIndicator( $inputId ) .
3633 Xml::tags( 'label', array( 'for' => $inputId ), $optionHTML ) .
3734 '&nbsp;' .
3835 "</div>\n";
@@ -39,44 +36,43 @@
4037 return $s;
4138 }
4239
43 - function submitForm() {
 40+ function submitQuestion( $question, $status ) {
4441 global $wgRequest;
45 - $questions = $this->election->getQuestions();
 42+
 43+ $options = $question->getOptions();
4644 $record = '';
47 - $status = Status::newGood();
 45+ $ok = true;
 46+ foreach ( $options as $option ) {
 47+ $id = 'securepoll_q' . $question->getId() . '_opt' . $option->getId();
 48+ $rank = $wgRequest->getVal( $id );
4849
49 - foreach ( $questions as $question ) {
50 - $options = $question->getOptions();
51 - foreach ( $options as $option ) {
52 - $id = 'securepoll_q' . $question->getId() . '_opt' . $option->getId();
53 - $rank = $wgRequest->getVal( $id );
54 -
55 - if ( is_numeric( $rank ) ) {
56 - if ( $rank <= 0 || $rank >= 1000 ) {
57 - $status->fatal( 'securepoll-invalid-rank', $id );
58 - continue;
59 - } else {
60 - $rank = intval( $rank );
61 - }
62 - } elseif ( strval( $rank ) === '' ) {
63 - if ( $this->election->getProperty( 'must-rank-all' ) ) {
64 - $status->fatal( 'securepoll-unranked-options', $id );
65 - continue;
66 - } else {
67 - $rank = 1000;
68 - }
 50+ if ( is_numeric( $rank ) ) {
 51+ if ( $rank <= 0 || $rank >= 1000 ) {
 52+ $status->sp_fatal( 'securepoll-invalid-rank', $id );
 53+ $ok = false;
 54+ continue;
6955 } else {
70 - $status->fatal( 'securepoll-invalid-rank', $id );
 56+ $rank = intval( $rank );
 57+ }
 58+ } elseif ( strval( $rank ) === '' ) {
 59+ if ( $this->election->getProperty( 'must-rank-all' ) ) {
 60+ $status->sp_fatal( 'securepoll-unranked-options', $id );
 61+ $ok = false;
7162 continue;
 63+ } else {
 64+ $rank = 1000;
7265 }
73 - $record .= sprintf( 'Q%08X-A%08X-R%08X--',
74 - $question->getId(), $option->getId(), $rank );
 66+ } else {
 67+ $status->sp_fatal( 'securepoll-invalid-rank', $id );
 68+ $ok = false;
 69+ continue;
7570 }
 71+ $record .= sprintf( 'Q%08X-A%08X-R%08X--',
 72+ $question->getId(), $option->getId(), $rank );
7673 }
77 - if ( $status->isOK() ) {
78 - $status->value = $record . "\n";
 74+ if ( $ok ) {
 75+ return $record;
7976 }
80 - return $status;
8177 }
8278
8379 function unpackRecord( $record ) {
@@ -97,7 +93,7 @@
9894 return $ranks;
9995 }
10096
101 - function convertScores( $scores, $options = array() ) {
 97+ function convertScores( $scores, $params = array() ) {
10298 $result = array();
10399 foreach ( $this->election->getQuestions() as $question ) {
104100 $qid = $question->getId();
Index: trunk/extensions/SecurePoll/includes/ballots/ApprovalBallot.php
@@ -0,0 +1,88 @@
 2+<?php
 3+
 4+/**
 5+ * Checkbox approval voting.
 6+ */
 7+class SecurePoll_ApprovalBallot extends SecurePoll_Ballot {
 8+ function getTallyTypes() {
 9+ return array( 'plurality' );
 10+ }
 11+
 12+ function getQuestionForm( $question, $options ) {
 13+ global $wgRequest;
 14+ $name = 'securepoll_q' . $question->getId();
 15+ $s = '';
 16+ foreach ( $options as $option ) {
 17+ $optionHTML = $option->parseMessageInline( 'text' );
 18+ $optionId = $option->getId();
 19+ $inputId = "{$name}_opt{$optionId}";
 20+ $oldValue = $wgRequest->getBool( $inputId );
 21+ $s .=
 22+ '<div class="securepoll-option-approval">' .
 23+ Xml::check( $inputId, $oldValue, array( 'id' => $inputId ) ) .
 24+ '&nbsp;' .
 25+ Xml::tags( 'label', array( 'for' => $inputId ), $optionHTML ) .
 26+ '&nbsp;' .
 27+ "</div>\n";
 28+ }
 29+ return $s;
 30+ }
 31+
 32+ function submitQuestion( $question, $status ) {
 33+ global $wgRequest;
 34+
 35+ $options = $question->getOptions();
 36+ $record = '';
 37+ foreach ( $options as $option ) {
 38+ $id = 'securepoll_q' . $question->getId() . '_opt' . $option->getId();
 39+ $checked = $wgRequest->getBool( $id );
 40+ $record .= sprintf( 'Q%08X-A%08X-%s--',
 41+ $question->getId(), $option->getId(), $checked ? 'y' : 'n' );
 42+ }
 43+ return $record;
 44+ }
 45+
 46+ function unpackRecord( $record ) {
 47+ $scores = array();
 48+ $itemLength = 2*8 + 7;
 49+ for ( $offset = 0; $offset < strlen( $record ); $offset += $itemLength ) {
 50+ if ( !preg_match( '/Q([0-9A-F]{8})-A([0-9A-F]{8})-([yn])--/A',
 51+ $record, $m, 0, $offset ) )
 52+ {
 53+ wfDebug( __METHOD__.": regex doesn't match\n" );
 54+ return false;
 55+ }
 56+ $qid = intval( base_convert( $m[1], 16, 10 ) );
 57+ $oid = intval( base_convert( $m[2], 16, 10 ) );
 58+ $score = ( $m[3] === 'y' ) ? 1 : 0;
 59+ $scores[$qid][$oid] = $score;
 60+ }
 61+ return $scores;
 62+ }
 63+
 64+ function convertScores( $scores, $params = array() ) {
 65+ $result = array();
 66+ foreach ( $this->election->getQuestions() as $question ) {
 67+ $qid = $question->getId();
 68+ if ( !isset( $scores[$qid] ) ) {
 69+ return false;
 70+ }
 71+ $s = '';
 72+ $qscores = $scores[$qid];
 73+ ksort( $qscores );
 74+ $first = true;
 75+ foreach ( $qscores as $score ) {
 76+ if ( $first ) {
 77+ $first = false;
 78+ } else {
 79+ $s .= ', ';
 80+ }
 81+ $s .= $score ? 'y' : 'n';
 82+ }
 83+ $result[$qid] = $s;
 84+ }
 85+ return $result;
 86+ }
 87+
 88+}
 89+
Property changes on: trunk/extensions/SecurePoll/includes/ballots/ApprovalBallot.php
___________________________________________________________________
Name: svn:eol-style
190 + native
Index: trunk/extensions/SecurePoll/includes/entities/Entity.php
@@ -209,25 +209,25 @@
210210 /**
211211 * Get configuration XML. Overridden by most subclasses.
212212 */
213 - function getConfXml( $options = array() ) {
 213+ function getConfXml( $params = array() ) {
214214 return "<{$this->type}>\n" .
215 - $this->getConfXmlEntityStuff( $options ) .
 215+ $this->getConfXmlEntityStuff( $params ) .
216216 "</{$this->type}>\n";
217217 }
218218
219219 /**
220220 * Get an XML snippet giving the messages and properties
221221 */
222 - function getConfXmlEntityStuff( $options = array() ) {
 222+ function getConfXmlEntityStuff( $params = array() ) {
223223 $s = Xml::element( 'id', array(), $this->getId() ) . "\n";
224 - $blacklist = $this->getPropertyDumpBlacklist( $options );
 224+ $blacklist = $this->getPropertyDumpBlacklist( $params );
225225 foreach ( $this->getAllProperties() as $name => $value ) {
226226 if ( !in_array( $name, $blacklist ) ) {
227227 $s .= Xml::element( 'property', array( 'name' => $name ), $value ) . "\n";
228228 }
229229 }
230 - if ( isset( $options['langs'] ) ) {
231 - $langs = $options['langs'];
 230+ if ( isset( $params['langs'] ) ) {
 231+ $langs = $params['langs'];
232232 } else {
233233 $langs = $this->context->languages;
234234 }
@@ -250,7 +250,7 @@
251251 * Get property names which aren't included in an XML dump.
252252 * Overloaded by Election.
253253 */
254 - function getPropertyDumpBlacklist( $options = array() ) {
 254+ function getPropertyDumpBlacklist( $params = array() ) {
255255 return array();
256256 }
257257
Index: trunk/extensions/SecurePoll/includes/entities/Election.php
@@ -323,7 +323,7 @@
324324 /**
325325 * Get an XML snippet describing the configuration of this object
326326 */
327 - function getConfXml( $options = array() ) {
 327+ function getConfXml( $params = array() ) {
328328 $s = "<configuration>\n" .
329329 Xml::element( 'title', array(), $this->title ) . "\n" .
330330 Xml::element( 'ballot', array(), $this->ballotType ) . "\n" .
@@ -331,11 +331,11 @@
332332 Xml::element( 'primaryLang', array(), $this->primaryLang ) . "\n" .
333333 Xml::element( 'startDate', array(), wfTimestamp( TS_ISO_8601, $this->startDate ) ) . "\n" .
334334 Xml::element( 'endDate', array(), wfTimestamp( TS_ISO_8601, $this->endDate ) ) . "\n" .
335 - $this->getConfXmlEntityStuff( $options );
 335+ $this->getConfXmlEntityStuff( $params );
336336
337337 # If we're making a jump dump, we need to add some extra properties, and
338338 # override the auth type
339 - if ( !empty( $options['jump'] ) ) {
 339+ if ( !empty( $params['jump'] ) ) {
340340 $s .=
341341 Xml::element( 'auth', array(), 'local' ) . "\n" .
342342 Xml::element( 'property',
@@ -351,7 +351,7 @@
352352 }
353353
354354 foreach ( $this->getQuestions() as $question ) {
355 - $s .= $question->getConfXml( $options );
 355+ $s .= $question->getConfXml( $params );
356356 }
357357 $s .= "</configuration>\n";
358358 return $s;
@@ -360,8 +360,8 @@
361361 /**
362362 * Get property names which aren't included in an XML dump
363363 */
364 - function getPropertyDumpBlacklist( $options = array() ) {
365 - if ( !empty( $options['jump'] ) ) {
 364+ function getPropertyDumpBlacklist( $params = array() ) {
 365+ if ( !empty( $params['jump'] ) ) {
366366 return array(
367367 'gpg-encrypt-key',
368368 'gpg-sign-key',
Index: trunk/extensions/SecurePoll/includes/entities/Question.php
@@ -38,10 +38,10 @@
3939 return $this->options;
4040 }
4141
42 - function getConfXml( $options = array() ) {
43 - $s = "<question>\n" . $this->getConfXmlEntityStuff( $options );
 42+ function getConfXml( $params = array() ) {
 43+ $s = "<question>\n" . $this->getConfXmlEntityStuff( $params );
4444 foreach ( $this->getOptions() as $option ) {
45 - $s .= $option->getConfXml( $options );
 45+ $s .= $option->getConfXml( $params );
4646 }
4747 $s .= "</question>\n";
4848 return $s;
Index: trunk/extensions/SecurePoll/includes/main/Context.php
@@ -82,11 +82,13 @@
8383 /** Set the store class */
8484 function setStoreClass( $class ) {
8585 $this->store = null;
 86+ $this->messageCache = $this->messagesLoaded = array();
8687 $this->storeClass = $class;
8788 }
8889
8990 /** Set the store object. Overrides any previous store class. */
9091 function setStore( $store ) {
 92+ $this->messageCache = $this->messagesLoaded = array();
9193 $this->store = $store;
9294 }
9395
@@ -311,4 +313,9 @@
312314 echo $s;
313315 }
314316 }
 317+
 318+ function getResourceUrl( $resource ) {
 319+ global $wgScriptPath;
 320+ return "$wgScriptPath/extensions/SecurePoll/resources/$resource";
 321+ }
315322 }
Index: trunk/extensions/SecurePoll/resources/SecurePoll.css
@@ -45,23 +45,52 @@
4646 .securepoll-option-preferential {
4747 margin-bottom: 0.5em;
4848 }
49 -.securepoll-results {
 49+
 50+.securepoll-table {
5051 margin: 1em 1em 1em 0;
5152 background: #f9f9f9;
52 - border: 1px #aaa solid;
 53+ border: thin #aaa solid;
5354 border-collapse: collapse;
5455 }
55 -.securepoll-results th, .securepoll-results td {
56 - border: 1px #aaa solid;
 56+.securepoll-table th, .securepoll-table td {
 57+ border: thin #aaa solid;
5758 padding: 0.4em;
5859 }
59 -.securepoll-results th {
 60+.securepoll-table th {
6061 background: #f2f2f2;
6162 text-align: center;
6263 }
63 -.securepoll-results caption {
 64+.securepoll-table caption {
6465 font-weight: bold;
6566 }
66 -.securepoll-results-row-heading {
67 - background: #f2f2f2;
 67+
 68+.securepoll-ballot-table {
 69+ border-collapse: collapse;
6870 }
 71+.securepoll-ballot-table th {
 72+ font-weight: bold;
 73+ text-align: center;
 74+ border: thin #999 solid;
 75+ padding: 0 1.5em 0 1.5em;
 76+}
 77+.securepoll-ballot-table td {
 78+ text-align: center;
 79+ border-left: thin #999 solid;
 80+ border-right: thin #999 solid;
 81+ border-bottom: thin #bbb dotted;
 82+ padding: 0 1.5em 0 1.5em;
 83+}
 84+.securepoll-ballot-table td.securepoll-ballot-optlabel { /* High specificity */
 85+ text-align: left;
 86+}
 87+.securepoll-ballot-row:hover {
 88+ background: #eeeeff;
 89+}
 90+.securepoll-error-location {
 91+ margin: 1px 1em 1px 1px;
 92+ border: 2px transparent solid;
 93+ padding: 1px;
 94+}
 95+.securepoll-error-jump {
 96+ margin-right: 0.5em;
 97+}
Index: trunk/extensions/SecurePoll/resources/SecurePoll.js
@@ -109,3 +109,53 @@
110110 securepoll_strike_popup( event, action == 'strike' ? 'unstrike' : 'strike', voteId );
111111 }
112112 }
 113+
 114+function securepoll_ballot_setup() {
 115+ if ( !document.getElementsByTagName ) {
 116+ return;
 117+ }
 118+ var anchors = document.getElementsByTagName( 'a' );
 119+ for ( var i = 0; i < anchors.length; i++ ) {
 120+ var elt = anchors.item( i );
 121+ if ( elt.className != 'securepoll-error-jump' ) {
 122+ continue;
 123+ }
 124+ if ( elt.addEventListener ) {
 125+ elt.addEventListener( 'click',
 126+ function() {
 127+ securepoll_error_jump( this, anchors );
 128+ },
 129+ false );
 130+ } else {
 131+ elt.attachEvent( 'onclick', securepoll_error_jump );
 132+ }
 133+ }
 134+}
 135+
 136+/**
 137+ * TODO: make prettier
 138+ */
 139+function securepoll_error_jump( source, anchors ) {
 140+ for ( var i = 0; i < anchors.length; i++ ) {
 141+ var anchor = anchors.item( i );
 142+ if ( anchor.className != 'securepoll-error-jump' ) {
 143+ continue;
 144+ }
 145+ var id = anchor.getAttribute( 'href' ).substr( 1 );
 146+ var elt = document.getElementById( id );
 147+ if ( !elt ) {
 148+ continue;
 149+ }
 150+
 151+ try {
 152+ if ( anchor == source ) {
 153+ elt.style.borderColor = '#ff0000';
 154+ elt.style.backgroundColor = '#ffcc99';
 155+ } else {
 156+ elt.style.backgroundColor = 'transparent';
 157+ elt.style.borderColor = 'transparent';
 158+ }
 159+ } catch ( e ) {}
 160+ }
 161+}
 162+
Index: trunk/extensions/SecurePoll/resources/warning-22.png
Cannot display: file marked as a binary type.
svn:mime-type = image/png
Property changes on: trunk/extensions/SecurePoll/resources/warning-22.png
___________________________________________________________________
Name: svn:mime-type
113163 + image/png
Index: trunk/extensions/SecurePoll/resources/down-16.png
Cannot display: file marked as a binary type.
svn:mime-type = image/png
Property changes on: trunk/extensions/SecurePoll/resources/down-16.png
___________________________________________________________________
Name: svn:mime-type
114164 + image/png
Index: trunk/extensions/SecurePoll/SecurePoll.sql
@@ -90,6 +90,7 @@
9191
9292 -- Options for answering a given question, see Option.php
9393 -- FIXME: needs op_election index for import.php
 94+-- FIXME: needs op_index column for determining the order if shuffle is off
9495 CREATE TABLE /*_*/securepoll_options (
9596 -- securepoll_entity.en_id
9697 op_entity int not null primary key,

Status & tagging log