Index: trunk/extensions/SecurePoll/includes/Store.php |
— | — | @@ -22,6 +22,12 @@ |
23 | 23 | function getMessages( $lang, $ids ); |
24 | 24 | |
25 | 25 | /** |
| 26 | + * Get a list of languages that the given entity IDs have messages for. |
| 27 | + * Returns an array of language codes. |
| 28 | + */ |
| 29 | + function getLangList( $ids ); |
| 30 | + |
| 31 | + /** |
26 | 32 | * Get an array of properties for a given set of IDs. Returns a 2-d array |
27 | 33 | * mapping IDs and property keys to values. |
28 | 34 | */ |
— | — | @@ -84,6 +90,22 @@ |
85 | 91 | return $messages; |
86 | 92 | } |
87 | 93 | |
| 94 | + function getLangList( $ids ) { |
| 95 | + $db = $this->getDB(); |
| 96 | + $res = $db->select( |
| 97 | + 'securepoll_msgs', |
| 98 | + 'DISTINCT msg_lang', |
| 99 | + array( |
| 100 | + 'msg_entity' => $ids |
| 101 | + ), |
| 102 | + __METHOD__ ); |
| 103 | + $langs = array(); |
| 104 | + foreach ( $res as $row ) { |
| 105 | + $langs[] = $row->msg_lang; |
| 106 | + } |
| 107 | + return $langs; |
| 108 | + } |
| 109 | + |
88 | 110 | function getProperties( $ids ) { |
89 | 111 | $db = $this->getDB(); |
90 | 112 | $res = $db->select( |
— | — | @@ -237,6 +259,19 @@ |
238 | 260 | return array_intersect_key( $this->messages[$lang], array_flip( $ids ) ); |
239 | 261 | } |
240 | 262 | |
| 263 | + function getLangList( $ids ) { |
| 264 | + $langs = array(); |
| 265 | + foreach ( $this->messages as $lang => $langMessages ) { |
| 266 | + foreach ( $ids as $id ) { |
| 267 | + if ( isset( $langMessages[$id] ) ) { |
| 268 | + $langs[] = $lang; |
| 269 | + break; |
| 270 | + } |
| 271 | + } |
| 272 | + } |
| 273 | + return $langs; |
| 274 | + } |
| 275 | + |
241 | 276 | function getProperties( $ids ) { |
242 | 277 | $ids = (array)$ids; |
243 | 278 | return array_intersect_key( $this->properties, array_flip( $ids ) ); |
Index: trunk/extensions/SecurePoll/includes/Election.php |
— | — | @@ -309,7 +309,7 @@ |
310 | 310 | /** |
311 | 311 | * Get an XML snippet describing the configuration of this object |
312 | 312 | */ |
313 | | - function getConfXml() { |
| 313 | + function getConfXml( $options = array() ) { |
314 | 314 | $s = "<configuration>\n" . |
315 | 315 | Xml::element( 'title', array(), $this->title ) . "\n" . |
316 | 316 | Xml::element( 'ballot', array(), $this->ballotType ) . "\n" . |
— | — | @@ -317,10 +317,27 @@ |
318 | 318 | Xml::element( 'primaryLang', array(), $this->primaryLang ) . "\n" . |
319 | 319 | Xml::element( 'startDate', array(), wfTimestamp( TS_ISO_8601, $this->startDate ) ) . "\n" . |
320 | 320 | Xml::element( 'endDate', array(), wfTimestamp( TS_ISO_8601, $this->endDate ) ) . "\n" . |
321 | | - Xml::element( 'auth', array(), $this->authType ) . "\n" . |
322 | | - $this->getConfXmlEntityStuff(); |
| 321 | + $this->getConfXmlEntityStuff( $options ); |
| 322 | + |
| 323 | + # If we're making a jump dump, we need to add some extra properties, and |
| 324 | + # override the auth type |
| 325 | + if ( !empty( $options['jump'] ) ) { |
| 326 | + $s .= |
| 327 | + Xml::element( 'auth', array(), 'local' ) . "\n" . |
| 328 | + Xml::element( 'property', |
| 329 | + array( 'name' => 'jump-url' ), |
| 330 | + $this->context->getSpecialTitle()->getFullURL() |
| 331 | + ) . "\n" . |
| 332 | + Xml::element( 'property', |
| 333 | + array( 'name' => 'jump-id' ), |
| 334 | + $this->getId() |
| 335 | + ) . "\n"; |
| 336 | + } else { |
| 337 | + $s .= Xml::element( 'auth', array(), $this->authType ) . "\n"; |
| 338 | + } |
| 339 | + |
323 | 340 | foreach ( $this->getQuestions() as $question ) { |
324 | | - $s .= $question->getConfXml(); |
| 341 | + $s .= $question->getConfXml( $options ); |
325 | 342 | } |
326 | 343 | $s .= "</configuration>\n"; |
327 | 344 | return $s; |
Index: trunk/extensions/SecurePoll/includes/Question.php |
— | — | @@ -38,10 +38,10 @@ |
39 | 39 | return $this->options; |
40 | 40 | } |
41 | 41 | |
42 | | - function getConfXml() { |
43 | | - $s = "<question>\n" . $this->getConfXmlEntityStuff(); |
| 42 | + function getConfXml( $options = array() ) { |
| 43 | + $s = "<question>\n" . $this->getConfXmlEntityStuff( $options ); |
44 | 44 | foreach ( $this->getOptions() as $option ) { |
45 | | - $s .= $option->getConfXml(); |
| 45 | + $s .= $option->getConfXml( $options ); |
46 | 46 | } |
47 | 47 | $s .= "</question>\n"; |
48 | 48 | return $s; |
Index: trunk/extensions/SecurePoll/includes/Entity.php |
— | — | @@ -168,6 +168,18 @@ |
169 | 169 | } |
170 | 170 | |
171 | 171 | /** |
| 172 | + * Get a list of languages for which we have translations, for this entity |
| 173 | + * and its descendants. |
| 174 | + */ |
| 175 | + function getLangList() { |
| 176 | + $ids = array( $this->getId() ); |
| 177 | + foreach ( $this->getDescendants() as $child ) { |
| 178 | + $ids[] = $child->getId(); |
| 179 | + } |
| 180 | + return $this->context->getStore()->getLangList( $ids ); |
| 181 | + } |
| 182 | + |
| 183 | + /** |
172 | 184 | * Get a property value. If it does not exist, the $default parameter |
173 | 185 | * is passed back. |
174 | 186 | * @param $name string |
— | — | @@ -197,24 +209,35 @@ |
198 | 210 | /** |
199 | 211 | * Get configuration XML. Overridden by most subclasses. |
200 | 212 | */ |
201 | | - function getConfXml() { |
| 213 | + function getConfXml( $options = array() ) { |
202 | 214 | return "<{$this->type}>\n" . |
203 | | - $this->getConfXmlEntityStuff() . |
| 215 | + $this->getConfXmlEntityStuff( $options ) . |
204 | 216 | "</{$this->type}>\n"; |
205 | 217 | } |
206 | 218 | |
207 | 219 | /** |
208 | 220 | * Get an XML snippet giving the messages and properties |
209 | 221 | */ |
210 | | - function getConfXmlEntityStuff() { |
| 222 | + function getConfXmlEntityStuff( $options = array() ) { |
211 | 223 | $s = Xml::element( 'id', array(), $this->getId() ) . "\n"; |
212 | 224 | foreach ( $this->getAllProperties() as $name => $value ) { |
213 | 225 | $s .= Xml::element( 'property', array( 'name' => $name ), $value ) . "\n"; |
214 | 226 | } |
| 227 | + if ( isset( $options['langs'] ) ) { |
| 228 | + $langs = $options['langs']; |
| 229 | + } else { |
| 230 | + $langs = $this->context->languages; |
| 231 | + } |
215 | 232 | foreach ( $this->getMessageNames() as $name ) { |
216 | | - foreach ( $this->context->languages as $lang ) { |
217 | | - $s .= Xml::element( 'message', array( 'name' => $name, 'lang' => $lang ), |
218 | | - $this->getRawMessage( $name, $lang ) ) . "\n"; |
| 233 | + foreach ( $langs as $lang ) { |
| 234 | + $value = $this->getRawMessage( $name, $lang ); |
| 235 | + if ( $value !== false ) { |
| 236 | + $s .= Xml::element( |
| 237 | + 'message', |
| 238 | + array( 'name' => $name, 'lang' => $lang ), |
| 239 | + $value |
| 240 | + ) . "\n"; |
| 241 | + } |
219 | 242 | } |
220 | 243 | } |
221 | 244 | return $s; |
Index: trunk/extensions/SecurePoll/includes/Context.php |
— | — | @@ -74,6 +74,11 @@ |
75 | 75 | return $this->store; |
76 | 76 | } |
77 | 77 | |
| 78 | + /** Get a Title object for Special:SecurePoll */ |
| 79 | + function getSpecialTitle( $subpage = false ) { |
| 80 | + return SpecialPage::getTitleFor( 'SecurePoll', $subpage ); |
| 81 | + } |
| 82 | + |
78 | 83 | /** Set the store class */ |
79 | 84 | function setStoreClass( $class ) { |
80 | 85 | $this->store = null; |
Index: trunk/extensions/SecurePoll/cli/import.php |
— | — | @@ -0,0 +1,257 @@ |
| 2 | +<?php |
| 3 | + |
| 4 | +require( dirname( __FILE__ ) . '/cli.inc' ); |
| 5 | + |
| 6 | +$usage = <<<EOT |
| 7 | +Import configuration files into the local SecurePoll database. Files can be |
| 8 | +generated with dump.php. |
| 9 | + |
| 10 | +Usage: import.php [options] <file> |
| 11 | + |
| 12 | +Options are: |
| 13 | + --update-msgs Update the internationalised text for the elections, do |
| 14 | + not update configuration. |
| 15 | + |
| 16 | + --replace If an election with a conflicting title exists already, |
| 17 | + replace it, updating its configuration. The default is |
| 18 | + to exit with an error. |
| 19 | + |
| 20 | +Note that any vote records will NOT be imported. |
| 21 | + |
| 22 | +For the moment, the entity IDs are preserved, to allow easier implementation of |
| 23 | +the message update feature. This means conflicting entity IDs in the local |
| 24 | +database will generate an error. This restriction will be removed in the |
| 25 | +future. |
| 26 | + |
| 27 | +EOT; |
| 28 | + |
| 29 | +# Most of the code here will eventually be refactored into the update interfaces |
| 30 | +# of the entity and context classes, but that project can wait until we have a |
| 31 | +# setup UI. |
| 32 | + |
| 33 | +if ( !isset( $args[0] ) ) { |
| 34 | + echo $usage; |
| 35 | + exit( 1 ); |
| 36 | +} |
| 37 | +if ( !file_exists( $args[0] ) ) { |
| 38 | + echo "The specified file \"{$args[0]}\" does not exist\n"; |
| 39 | + exit( 1 ); |
| 40 | +} |
| 41 | + |
| 42 | +foreach ( array( 'update-msgs', 'replace' ) as $optName ) { |
| 43 | + if ( !isset( $options[$optName] ) ) { |
| 44 | + $options[$optName] = false; |
| 45 | + } |
| 46 | +} |
| 47 | + |
| 48 | +$success = spImportDump( $args[0], $options ); |
| 49 | +exit( $success ? 0 : 1 ); |
| 50 | + |
| 51 | +function spImportDump( $fileName, $options ) { |
| 52 | + $store = new SecurePoll_XMLStore( $fileName ); |
| 53 | + $success = $store->readFile(); |
| 54 | + if ( !$success ) { |
| 55 | + echo "Error reading XML dump, possibly corrupt\n"; |
| 56 | + return false; |
| 57 | + } |
| 58 | + $electionIds = $store->getAllElectionIds(); |
| 59 | + if ( !count( $electionIds ) ) { |
| 60 | + echo "No elections found to import.\n"; |
| 61 | + return true; |
| 62 | + } |
| 63 | + |
| 64 | + $xc = new SecurePoll_Context; |
| 65 | + $xc->setStore( $store ); |
| 66 | + $dbw = wfGetDB( DB_MASTER ); |
| 67 | + |
| 68 | + # Start the configuration transaction |
| 69 | + $dbw->begin(); |
| 70 | + foreach ( $electionIds as $id ) { |
| 71 | + $elections = $store->getElectionInfo( array( $id ) ); |
| 72 | + $electionInfo = reset( $elections ); |
| 73 | + $existingId = $dbw->selectField( |
| 74 | + 'securepoll_elections', |
| 75 | + 'el_entity', |
| 76 | + array( 'el_title' => $electionInfo['title'] ), |
| 77 | + __METHOD__, |
| 78 | + array( 'FOR UPDATE' ) ); |
| 79 | + if ( $existingId !== false ) { |
| 80 | + if ( $options['replace'] ) { |
| 81 | + spDeleteElection( $existingId ); |
| 82 | + $success = spImportConfiguration( $store, $electionInfo ); |
| 83 | + } elseif ( $options['update-msgs'] ) { |
| 84 | + # Do the message update and move on to the next election |
| 85 | + $success = spUpdateMessages( $store, $electionInfo ); |
| 86 | + } else { |
| 87 | + echo "Conflicting election title found \"{$electionInfo['title']}\"\n"; |
| 88 | + echo "Use --replace to replace the existing election.\n"; |
| 89 | + $success = false; |
| 90 | + } |
| 91 | + } elseif ( $options['update-msgs'] ) { |
| 92 | + echo "Cannot update messages: election \"{$electionInfo['title']}\" not found.\n"; |
| 93 | + echo "Import the configuration first, without the --update-msgs switch.\n"; |
| 94 | + $success = false; |
| 95 | + } else { |
| 96 | + $success = spImportConfiguration( $store, $electionInfo ); |
| 97 | + } |
| 98 | + if ( !$success ) { |
| 99 | + $dbw->rollback(); |
| 100 | + return false; |
| 101 | + } |
| 102 | + } |
| 103 | + $dbw->commit(); |
| 104 | + return true; |
| 105 | +} |
| 106 | + |
| 107 | +function spDeleteElection( $electionId ) { |
| 108 | + $dbw = wfGetDB( DB_MASTER ); |
| 109 | + |
| 110 | + # Get a list of entity IDs and lock them |
| 111 | + $questionIds = array(); |
| 112 | + $res = $dbw->select( 'securepoll_questions', array( 'qu_entity' ), |
| 113 | + array( 'qu_election' => $electionId ), |
| 114 | + __METHOD__, array( 'FOR UPDATE' ) ); |
| 115 | + foreach ( $res as $row ) { |
| 116 | + $questionIds[] = $row->qu_entity; |
| 117 | + } |
| 118 | + |
| 119 | + $res = $dbw->select( 'securepoll_options', array( 'op_entity' ), |
| 120 | + array( 'op_election' => $electionId ), |
| 121 | + __METHOD__, array( 'FOR UPDATE' ) ); |
| 122 | + $optionIds = array(); |
| 123 | + foreach ( $res as $row ) { |
| 124 | + $optionIds[] = $row->op_entity; |
| 125 | + } |
| 126 | + |
| 127 | + $entityIds = array_merge( $optionIds, $questionIds, array( $electionId ) ); |
| 128 | + |
| 129 | + # Delete the messages and properties |
| 130 | + $dbw->delete( 'securepoll_msgs', array( 'msg_entity' => $entityIds ) ); |
| 131 | + $dbw->delete( 'securepoll_properties', array( 'pr_entity' => $entityIds ) ); |
| 132 | + |
| 133 | + # Delete the entities |
| 134 | + $dbw->delete( 'securepoll_options', array( 'op_entity' => $optionIds ), __METHOD__ ); |
| 135 | + $dbw->delete( 'securepoll_questions', array( 'qu_entity' => $questionIds ), __METHOD__ ); |
| 136 | + $dbw->delete( 'securepoll_elections', array( 'el_entity' => $electionId ), __METHOD__ ); |
| 137 | + $dbw->delete( 'securepoll_entity', array( 'en_id' => $entityIds ), __METHOD__ ); |
| 138 | +} |
| 139 | + |
| 140 | +function spInsertEntity( $type, $id ) { |
| 141 | + $dbw = wfGetDB( DB_MASTER ); |
| 142 | + $dbw->insert( 'securepoll_entity', |
| 143 | + array( |
| 144 | + 'en_id' => $id, |
| 145 | + 'en_type' => $type, |
| 146 | + ), |
| 147 | + __METHOD__ |
| 148 | + ); |
| 149 | +} |
| 150 | + |
| 151 | +function spImportConfiguration( $store, $electionInfo ) { |
| 152 | + $dbw = wfGetDB( DB_MASTER ); |
| 153 | + $sourceIds = array(); |
| 154 | + |
| 155 | + # Election |
| 156 | + spInsertEntity( 'election', $electionInfo['id'] ); |
| 157 | + $dbw->insert( 'securepoll_elections', |
| 158 | + array( |
| 159 | + 'el_entity' => $electionInfo['id'], |
| 160 | + 'el_title' => $electionInfo['title'], |
| 161 | + 'el_ballot' => $electionInfo['ballot'], |
| 162 | + 'el_tally' => $electionInfo['tally'], |
| 163 | + 'el_primary_lang' => $electionInfo['primaryLang'], |
| 164 | + 'el_start_date' => $electionInfo['startDate'], |
| 165 | + 'el_end_date' => $electionInfo['endDate'], |
| 166 | + 'el_auth_type' => $electionInfo['auth'] |
| 167 | + ), |
| 168 | + __METHOD__ ); |
| 169 | + $sourceIds[] = $electionInfo['id']; |
| 170 | + |
| 171 | + |
| 172 | + # Questions |
| 173 | + $index = 1; |
| 174 | + foreach ( $electionInfo['questions'] as $questionInfo ) { |
| 175 | + spInsertEntity( 'question', $questionInfo['id'] ); |
| 176 | + $dbw->insert( 'securepoll_questions', |
| 177 | + array( |
| 178 | + 'qu_entity' => $questionInfo['id'], |
| 179 | + 'qu_election' => $electionInfo['id'], |
| 180 | + 'qu_index' => $index++, |
| 181 | + ), |
| 182 | + __METHOD__ ); |
| 183 | + $sourceIds[] = $questionInfo['id']; |
| 184 | + |
| 185 | + # Options |
| 186 | + $insertBatch = array(); |
| 187 | + foreach ( $questionInfo['options'] as $optionInfo ) { |
| 188 | + spInsertEntity( 'option', $optionInfo['id'] ); |
| 189 | + $insertBatch[] = array( |
| 190 | + 'op_entity' => $optionInfo['id'], |
| 191 | + 'op_election' => $electionInfo['id'], |
| 192 | + 'op_question' => $questionInfo['id'] |
| 193 | + ); |
| 194 | + $sourceIds[] = $optionInfo['id']; |
| 195 | + } |
| 196 | + $dbw->insert( 'securepoll_options', $insertBatch, __METHOD__ ); |
| 197 | + } |
| 198 | + |
| 199 | + # Messages |
| 200 | + spInsertMessages( $store, $sourceIds ); |
| 201 | + |
| 202 | + # Properties |
| 203 | + $properties = $store->getProperties( $sourceIds ); |
| 204 | + $insertBatch = array(); |
| 205 | + foreach ( $properties as $id => $entityProps ) { |
| 206 | + foreach ( $entityProps as $key => $value ) { |
| 207 | + $insertBatch[] = array( |
| 208 | + 'pr_entity' => $id, |
| 209 | + 'pr_key' => $key, |
| 210 | + 'pr_value' => $value |
| 211 | + ); |
| 212 | + } |
| 213 | + } |
| 214 | + if ( $insertBatch ) { |
| 215 | + $dbw->insert( 'securepoll_properties', $insertBatch, __METHOD__ ); |
| 216 | + } |
| 217 | + return true; |
| 218 | +} |
| 219 | + |
| 220 | +function spInsertMessages( $store, $entityIds ) { |
| 221 | + $langs = $store->getLangList( $entityIds ); |
| 222 | + $insertBatch = array(); |
| 223 | + foreach ( $langs as $lang ) { |
| 224 | + $messages = $store->getMessages( $lang, $entityIds ); |
| 225 | + foreach ( $messages as $id => $entityMsgs ) { |
| 226 | + foreach ( $entityMsgs as $key => $text ) { |
| 227 | + $insertBatch[] = array( |
| 228 | + 'msg_entity' => $id, |
| 229 | + 'msg_lang' => $lang, |
| 230 | + 'msg_key' => $key, |
| 231 | + 'msg_text' => $text |
| 232 | + ); |
| 233 | + } |
| 234 | + } |
| 235 | + } |
| 236 | + if ( $insertBatch ) { |
| 237 | + $dbw = wfGetDB( DB_MASTER ); |
| 238 | + $dbw->insert( 'securepoll_msgs', $insertBatch, __METHOD__ ); |
| 239 | + } |
| 240 | +} |
| 241 | + |
| 242 | +function spUpdateMessages( $store, $electionInfo ) { |
| 243 | + $entityIds = array( $electionInfo['id'] ); |
| 244 | + foreach ( $electionInfo['questions'] as $questionInfo ) { |
| 245 | + $entityIds[] = $questionInfo['id']; |
| 246 | + foreach ( $questionInfo['options'] as $optionInfo ) { |
| 247 | + $entityIds[] = $optionInfo['id']; |
| 248 | + } |
| 249 | + } |
| 250 | + |
| 251 | + # Delete existing messages |
| 252 | + $dbw = wfGetDB( DB_MASTER ); |
| 253 | + $dbw->delete( 'securepoll_msgs', array( 'msg_entity' => $entityIds ), __METHOD__ ); |
| 254 | + |
| 255 | + # Insert new messages |
| 256 | + spInsertMessages( $store, $entityIds ); |
| 257 | +} |
| 258 | + |
Property changes on: trunk/extensions/SecurePoll/cli/import.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 259 | + native |
Index: trunk/extensions/SecurePoll/cli/dump.php |
— | — | @@ -8,6 +8,15 @@ |
9 | 9 | $optionsWithArgs = array( 'o' ); |
10 | 10 | require( dirname(__FILE__).'/cli.inc' ); |
11 | 11 | |
| 12 | +$usage = <<<EOT |
| 13 | +Usage: php dump.php [options...] <election name> |
| 14 | +Options: |
| 15 | + -o <outfile> Output to the specified file |
| 16 | + --votes Include vote records |
| 17 | + --all-langs Include messages for all languages instead of just the primary |
| 18 | + --jump Produce a configuration dump suitable for setting up a jump wiki |
| 19 | +EOT; |
| 20 | + |
12 | 21 | if ( !isset( $args[0] ) ) { |
13 | 22 | spFatal( "Usage: php dump.php [-o <outfile>] <election name>" ); |
14 | 23 | } |
— | — | @@ -32,18 +41,28 @@ |
33 | 42 | spFatal( "Unable to open $fileName for writing" ); |
34 | 43 | } |
35 | 44 | |
36 | | -$context->setLanguages( array( $election->getLanguage() ) ); |
| 45 | +if ( isset( $options['all-langs'] ) ) { |
| 46 | + $langs = $election->getLangList(); |
| 47 | +} else { |
| 48 | + $langs = array( $election->getLanguage() ); |
| 49 | +} |
| 50 | +$confXml = $election->getConfXml( array( |
| 51 | + 'jump' => isset( $options['jump'] ), |
| 52 | + 'langs' => $langs |
| 53 | +) ); |
37 | 54 | |
38 | 55 | $cbdata = array( |
39 | | - 'header' => "<SecurePoll>\n<election>\n" . $election->getConfXml(), |
| 56 | + 'header' => "<SecurePoll>\n<election>\n$confXml", |
40 | 57 | 'outFile' => $outFile |
41 | 58 | ); |
| 59 | +$election->cbdata = $cbdata; |
42 | 60 | |
43 | 61 | # Write vote records |
44 | | -$election->cbdata = $cbdata; |
45 | | -$status = $election->dumpVotesToCallback( 'spDumpVote' ); |
46 | | -if ( !$status->isOK() ) { |
47 | | - spFatal( $status->getWikiText() ); |
| 62 | +if ( isset( $options['votes'] ) ) { |
| 63 | + $status = $election->dumpVotesToCallback( 'spDumpVote' ); |
| 64 | + if ( !$status->isOK() ) { |
| 65 | + spFatal( $status->getWikiText() ); |
| 66 | + } |
48 | 67 | } |
49 | 68 | if ( $election->cbdata['header'] ) { |
50 | 69 | fwrite( $outFile, $election->cbdata['header'] ); |
Index: trunk/extensions/SecurePoll/SecurePoll.sql |
— | — | @@ -89,6 +89,7 @@ |
90 | 90 | |
91 | 91 | |
92 | 92 | -- Options for answering a given question, see Option.php |
| 93 | +-- FIXME: needs op_election index for import.php |
93 | 94 | CREATE TABLE /*_*/securepoll_options ( |
94 | 95 | -- securepoll_entity.en_id |
95 | 96 | op_entity int not null primary key, |
— | — | @@ -99,6 +100,7 @@ |
100 | 101 | ) /*$wgDBTableOptions*/; |
101 | 102 | CREATE INDEX /*i*/spop_question ON /*_*/securepoll_options (op_question, op_entity); |
102 | 103 | |
| 104 | + |
103 | 105 | -- Voter list, independent for each election |
104 | 106 | -- See Voter.php |
105 | 107 | CREATE TABLE /*_*/securepoll_voters ( |