poll.module 27.8 KB
Newer Older
1
<?php
2
// $Id$
3

Dries's avatar
 
Dries committed
4 5 6 7 8 9
/**
 * @file
 * Enables your site to capture votes on different topics in the form of multiple
 * choice questions.
 */

10
/**
11
 * Implements hook_help().
12
 */
13 14
function poll_help($path, $arg) {
  switch ($path) {
Steven Wittens's avatar
Steven Wittens committed
15
    case 'admin/help#poll':
16 17
      $output = '';
      $output .= '<h3>' . t('About') . '</h3>';
18
      $output .= '<p>' . t('The Poll module can be used to create simple surveys or questionnaires that display cumulative results. A poll is a good way to receive feedback from site users and community members. For more information, see the online handbook entry for the <a href="@poll">Poll module</a>.', array('@poll' => 'http://drupal.org/handbook/modules/poll/')) . '</p>';
19 20 21
      $output .= '<h3>' . t('Uses') . '</h3>';
      $output .= '<dl>';
      $output .= '<dt>' . t('Creating a poll') . '</dt>';
22
      $output .= '<dd>' . t('Users can create a poll by clicking on Poll on the <a href="@add-content">Add new content</a> page, and entering the question being posed, the answer choices, and beginning vote counts for each choice. The status (closed or active) and duration (length of time the poll remains active for new votes) can also be specified.', array('@add-content' => url('node/add'))) . '</dd>';
23
      $output .= '<dt>' . t('Viewing polls') . '</dt>';
24
      $output .= '<dd>' . t('You can visit the <a href="@poll">Polls</a> page to view all current polls, or alternately enable the <em>Most recent poll</em> block on the <a href="@blocks">Blocks administration page</a>. To vote in or view the results of a specific poll, you can click on the poll itself.', array('@poll' => url('poll'), '@blocks' => url('admin/structure/block'))) . '</dd>';
25
      $output .= '</dl>';
26
      return $output;
Steven Wittens's avatar
Steven Wittens committed
27
  }
28 29
}

30
/**
31
 * Implements hook_init().
32 33
 */
function poll_init() {
34
  drupal_add_css(drupal_get_path('module', 'poll') . '/poll.css');
35 36
}

37
/**
38
 * Implements hook_theme().
39 40
 */
function poll_theme() {
41
  $theme_hooks = array(
42
    'poll_vote' => array(
43
      'template' => 'poll-vote',
44
      'render element' => 'form',
45
    ),
46
    'poll_choices' => array(
47
      'render element' => 'form',
48
    ),
49
    'poll_results' => array(
50
      'template' => 'poll-results',
51
      'variables' => array('raw_title' => NULL, 'results' => NULL, 'votes' => NULL, 'raw_links' => NULL, 'block' => NULL, 'nid' => NULL, 'vote' => NULL),
52 53
    ),
    'poll_bar' => array(
54
      'template' => 'poll-bar',
55
      'variables' => array('title' => NULL, 'votes' => NULL, 'total_votes' => NULL, 'vote' => NULL, 'block' => NULL),
56 57
    ),
  );
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
  // The theme system automatically discovers the theme's functions and
  // templates that implement more targeted "suggestions" of generic theme
  // hooks. But suggestions implemented by a module must be explicitly
  // registered.
  $theme_hooks += array(
    'poll_results__block' => array(
      'template' => 'poll-results--block',
      'variables' => $theme_hooks['poll_results']['variables'],
    ),
    'poll_bar__block' => array(
      'template' => 'poll-bar--block',
      'variables' => $theme_hooks['poll_bar']['variables'],
    ),
  );
  return $theme_hooks;
73 74
}

75
/**
76
 * Implements hook_permission().
77
 */
78
function poll_permission() {
79
  $perms = array(
80 81 82 83
    'vote on polls' => array(
      'title' => t('Vote on polls'),
    ),
    'cancel own vote' => array(
84
      'title' => t('Cancel and change own votes'),
85 86
    ),
    'inspect all votes' => array(
87
      'title' => t('View voting results'),
88
    ),
89 90 91
  );

  return $perms;
92 93 94
}

/**
95
 * Implements hook_menu().
96 97 98 99 100 101 102
 */
function poll_menu() {
  $items['poll'] = array(
    'title' => 'Polls',
    'page callback' => 'poll_page',
    'access arguments' => array('access content'),
    'type' => MENU_SUGGESTED_ITEM,
103
    'file' => 'poll.pages.inc',
104 105 106 107 108 109 110 111 112 113
  );

  $items['node/%node/votes'] = array(
    'title' => 'Votes',
    'page callback' => 'poll_votes',
    'page arguments' => array(1),
    'access callback' => '_poll_menu_access',
    'access arguments' => array(1, 'inspect all votes', FALSE),
    'weight' => 3,
    'type' => MENU_LOCAL_TASK,
114
    'file' => 'poll.pages.inc',
115 116 117 118 119 120 121 122 123 124
  );

  $items['node/%node/results'] = array(
    'title' => 'Results',
    'page callback' => 'poll_results',
    'page arguments' => array(1),
    'access callback' => '_poll_menu_access',
    'access arguments' => array(1, 'access content', TRUE),
    'weight' => 3,
    'type' => MENU_LOCAL_TASK,
125
    'file' => 'poll.pages.inc',
126
  );
127

128 129 130 131 132 133
  return $items;
}

/**
 * Callback function to see if a node is acceptable for poll menu items.
 */
134
function _poll_menu_access($node, $perm, $inspect_allowvotes) {
135 136 137
  return user_access($perm) && ($node->type == 'poll') && ($node->allowvotes || !$inspect_allowvotes);
}

138
/**
139
 * Implements hook_block_info().
140
 */
141
function poll_block_info() {
142 143
  $blocks['recent']['info'] = t('Most recent poll');
  return $blocks;
144 145 146
}

/**
147
 * Implements hook_block_view().
148 149 150
 *
 * Generates a block containing the latest poll.
 */
151
function poll_block_view($delta = '') {
Steven Wittens's avatar
Steven Wittens committed
152
  if (user_access('access content')) {
153
    // Retrieve the latest poll.
154 155 156 157 158 159 160 161 162 163
    $select = db_select('node', 'n');
    $select->join('poll', 'p', 'p.nid = n.nid');
    $select->fields('n', array('nid'))
      ->condition('n.status', 1)
      ->condition('p.active', 1)
      ->orderBy('n.created', 'DESC')
      ->range(0, 1)
      ->addTag('node_access');

    $record = $select->execute()->fetchObject();
164 165 166
    if ($record) {
      $poll = node_load($record->nid);
      if ($poll->nid) {
167
        $poll = poll_block_latest_poll_view($poll);
168
        $block['subject'] = t('Poll');
169
        $block['content'] = $poll->content;
170
        return $block;
Dries's avatar
 
Dries committed
171
      }
172 173 174 175
    }
  }
}

176
/**
177
 * Implements hook_cron().
178 179 180
 *
 * Closes polls that have exceeded their allowed runtime.
 */
181
function poll_cron() {
182 183 184 185 186 187
  $nids = db_query('SELECT p.nid FROM {poll} p INNER JOIN {node} n ON p.nid = n.nid WHERE (n.created + p.runtime) < :request_time AND p.active = :active AND p.runtime <> :runtime', array(':request_time' => REQUEST_TIME, ':active' => 1, ':runtime' => 0))->fetchCol();
  if (!empty($nids)) {
    db_update('poll')
      ->fields(array('active' => 0))
      ->condition('nid', $nids, 'IN')
      ->execute();
Dries's avatar
 
Dries committed
188
  }
189 190
}

191
/**
192
 * Implements hook_node_info().
193
 */
194 195 196 197
function poll_node_info() {
  return array(
    'poll' => array(
      'name' => t('Poll'),
198
      'base' => 'poll',
199
      'description' => t('A <em>poll</em> is a question with a set of possible responses. A <em>poll</em>, once created, automatically provides a simple running count of the number of votes received for each response.'),
200 201 202 203
      'title_label' => t('Question'),
      'has_body' => FALSE,
    )
  );
204 205
}

206
/**
207
 * Implements hook_field_extra_fields().
208
 */
209 210 211
function poll_field_extra_fields() {
  $extra['node']['poll'] = array(
    'choice_wrapper' => array(
212 213 214
      'label' => t('Poll choices'),
      'description' => t('Poll module choices.'),
      'weight' => -4,
215 216
    ),
    'settings' => array(
217 218 219
      'label' => t('Poll settings'),
      'description' => t('Poll module settings.'),
      'weight' => -3,
220 221
    ),
  );
222 223 224 225

  return $extra;
}

226
/**
227
 * Implements hook_form().
228
 */
229
function poll_form($node, &$form_state) {
230 231
  global $user;

232 233
  $admin = user_access('administer nodes') || user_access('edit any poll content') || (user_access('edit own poll content') && $user->uid == $node->uid);

234
  $type = node_type_get_type($node);
235

236
  $form_state['cache'] = TRUE;
237

238 239 240 241 242 243 244 245
  $form['title'] = array(
    '#type' => 'textfield',
    '#title' => check_plain($type->title_label),
    '#required' => TRUE,
    '#default_value' => $node->title,
    '#weight' => -5,
  );

246 247
  if (isset($form_state['choice_count'])) {
    $choice_count = $form_state['choice_count'];
248 249
  }
  else {
250
    $choice_count = max(2, empty($node->choice) ? 2 : count($node->choice));
251 252
  }

253 254 255
  // Add a wrapper for the choices and more button.
  $form['choice_wrapper'] = array(
    '#tree' => FALSE,
256
    '#weight' => -4,
257
    '#prefix' => '<div class="clearfix" id="poll-choice-wrapper">',
258
    '#suffix' => '</div>',
259
  );
260

261 262 263
  // Container for just the poll choices.
  $form['choice_wrapper']['choice'] = array(
    '#prefix' => '<div id="poll-choices">',
264
    '#suffix' => '</div>',
265
    '#theme' => 'poll_choices',
266 267
  );

268
  // Add the current choices to the form.
269 270 271 272 273 274
  $delta = 0;
  $weight = 0;
  if (isset($node->choice)) {
    $delta = count($node->choice);
    $weight = -$delta;
    foreach ($node->choice as $chid => $choice) {
275
      $key = 'chid:' . $chid;
276 277 278 279
      $form['choice_wrapper']['choice'][$key] = _poll_choice_form($key, $choice['chid'], $choice['chtext'], $choice['chvotes'], $choice['weight'], $choice_count);
      $weight = ($choice['weight'] > $weight) ? $choice['weight'] : $weight;
    }
  }
280

281 282 283
  // Add initial or additional choices.
  $existing_delta = $delta;
  for ($delta; $delta < $choice_count; $delta++) {
284
    $key = 'new:' . ($delta - $existing_delta);
285
    $form['choice_wrapper']['choice'][$key] = _poll_choice_form($key, NULL, '', 0, $weight, $choice_count);
286 287 288
  }

  // We name our button 'poll_more' to avoid conflicts with other modules using
289
  // AJAX-enabled buttons with the id 'more'.
290 291 292 293
  $form['choice_wrapper']['poll_more'] = array(
    '#type' => 'submit',
    '#value' => t('More choices'),
    '#description' => t("If the amount of boxes above isn't enough, click here to add more choices."),
294
    '#weight' => 1,
295 296
    '#limit_validation_errors' => array(array('choice')),
    '#submit' => array('poll_more_choices_submit'),
297
    '#ajax' => array(
298
      'callback' => 'poll_choice_js',
299 300 301
      'wrapper' => 'poll-choices',
      'effect' => 'fade',
    ),
302 303
  );

Steven Wittens's avatar
Steven Wittens committed
304
  // Poll attributes
305 306 307 308 309 310 311 312 313 314 315
  $duration = array(
    // 1-6 days.
    86400, 2 * 86400, 3 * 86400, 4 * 86400, 5 * 86400, 6 * 86400,
    // 1-3 weeks (7 days).
    604800, 2 * 604800, 3 * 604800,
    // 1-3,6,9 months (30 days).
    2592000, 2 * 2592000, 3 * 2592000, 6 * 2592000, 9 * 2592000,
    // 1 year (365 days).
    31536000,
  );
  $duration = array(0 => t('Unlimited')) + drupal_map_assoc($duration, 'format_interval');
316 317 318 319 320 321 322 323 324
  $active = array(0 => t('Closed'), 1 => t('Active'));

  $form['settings'] = array(
    '#type' => 'fieldset',
    '#collapsible' => TRUE,
    '#title' => t('Poll settings'),
    '#weight' => -3,
    '#access' => $admin,
  );
325

326 327 328 329 330 331 332 333
  $form['settings']['active'] = array(
    '#type' => 'radios',
    '#title' => t('Poll status'),
    '#default_value' => isset($node->active) ? $node->active : 1,
    '#options' => $active,
    '#description' => t('When a poll is closed, visitors can no longer vote for it.'),
    '#access' => $admin,
  );
334 335 336
  $form['settings']['runtime'] = array(
    '#type' => 'select',
    '#title' => t('Poll duration'),
337
    '#default_value' => isset($node->runtime) ? $node->runtime : 0,
338
    '#options' => $duration,
339 340
    '#description' => t('After this period, the poll will be closed automatically.'),
  );
Dries's avatar
 
Dries committed
341

342
  return $form;
343
}
344

345
/**
346 347 348 349 350 351 352
 * Submit handler to add more choices to a poll form.
 *
 * This handler is run regardless of whether JS is enabled or not. It makes
 * changes to the form state. If the button was clicked with JS disabled, then
 * the page is reloaded with the complete rebuilt form. If the button was
 * clicked with JS enabled, then ajax_form_callback() calls poll_choice_js() to
 * return just the changed part of the form.
353 354
 */
function poll_more_choices_submit($form, &$form_state) {
355
  include_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'node') . '/node.pages.inc';
356 357 358 359 360
  // Set the form to rebuild and run submit handlers.
  node_form_submit_build_node($form, $form_state);

  // Make the changes we want to the form state.
  if ($form_state['values']['poll_more']) {
361
    $n = $_GET['q'] == 'system/ajax' ? 1 : 5;
362
    $form_state['choice_count'] = count($form_state['values']['choice']) + $n;
363 364 365
  }
}

366
function _poll_choice_form($key, $chid = NULL, $value = '', $votes = 0, $weight = 0, $size = 10) {
367 368 369 370 371 372 373 374
  $admin = user_access('administer nodes');

  $form = array(
    '#tree' => TRUE,
  );

  // We'll manually set the #parents property of these fields so that
  // their values appear in the $form_state['values']['choice'] array.
375 376 377 378 379 380
  $form['chid'] = array(
    '#type' => 'value',
    '#value' => $chid,
    '#parents' => array('choice', $key, 'chid'),
  );

381 382 383
  $form['chtext'] = array(
    '#type' => 'textfield',
    '#default_value' => $value,
384
    '#parents' => array('choice', $key, 'chtext'),
385 386
  );

387 388 389 390 391 392 393 394 395 396 397 398 399 400 401
  $form['chvotes'] = array(
    '#type' => 'textfield',
    '#default_value' => $votes,
    '#size' => 5,
    '#maxlength' => 7,
    '#parents' => array('choice', $key, 'chvotes'),
    '#access' => user_access('administer nodes'),
  );

  $form['weight'] = array(
    '#type' => 'weight',
    '#default_value' => $weight,
    '#delta' => $size,
    '#parents' => array('choice', $key, 'weight'),
  );
402 403 404 405 406

  return $form;
}

/**
407 408 409 410 411 412
 * Ajax callback in response to new choices being added to the form.
 *
 * This returns the new page content to replace the page content made obsolete
 * by the form submission.
 *
 * @see poll_more_choices_submit()
413
 */
414
function poll_choice_js($form, $form_state) {
415
  return $form['choice_wrapper']['choice'];
416 417
}

418
/**
419
 * Renumber fields and create a teaser when a poll node is submitted.
420 421 422 423
 */
function poll_node_form_submit(&$form, &$form_state) {
  // Renumber fields
  $form_state['values']['choice'] = array_values($form_state['values']['choice']);
424
  $form_state['values']['teaser'] = poll_teaser((object) $form_state['values']);
425 426
}

Dries's avatar
 
Dries committed
427
/**
428
 * Implements hook_validate().
Dries's avatar
 
Dries committed
429
 */
430
function poll_validate($node, $form) {
431 432 433 434 435 436 437 438 439
  if (isset($node->title)) {
    // Check for at least two options and validate amount of votes:
    $realchoices = 0;
    // Renumber fields
    $node->choice = array_values($node->choice);
    foreach ($node->choice as $i => $choice) {
      if ($choice['chtext'] != '') {
        $realchoices++;
      }
440
      if (isset($choice['chvotes']) && $choice['chvotes'] < 0) {
441 442 443
        form_set_error("choice][$i][chvotes", t('Negative values are not allowed.'));
      }
    }
Dries's avatar
 
Dries committed
444

445 446 447 448
    if ($realchoices < 2) {
      form_set_error("choice][$realchoices][chtext", t('You must fill in at least two choices.'));
    }
  }
449 450
}

451
/**
452
 * Implements hook_node_prepare_translation().
453
 */
454
function poll_node_prepare_translation($node) {
455 456 457 458 459
  if ($node->type == 'poll') {
    $node->choice = $node->translation_source->choice;
  }
}

460
/**
461
 * Implements hook_load().
462
 */
463
function poll_load($nodes) {
464
  global $user;
465
  foreach ($nodes as $node) {
466
    $poll = db_query("SELECT runtime, active FROM {poll} WHERE nid = :nid", array(':nid' => $node->nid))->fetchObject();
467

468
    // Load the appropriate choices into the $poll object.
469 470 471
    $poll->choice = db_select('poll_choice', 'c')
      ->addTag('translatable')
      ->fields('c', array('chid', 'chtext', 'chvotes', 'weight'))
472
      ->condition('c.nid', $node->nid)
473
      ->orderBy('weight')
474
      ->execute()->fetchAllAssoc('chid', PDO::FETCH_ASSOC);
475

476 477 478 479
    // Determine whether or not this user is allowed to vote.
    $poll->allowvotes = FALSE;
    if (user_access('vote on polls') && $poll->active) {
      if ($user->uid) {
480 481 482 483 484
        $poll->vote = db_query('SELECT chid FROM {poll_vote} WHERE nid = :nid AND uid = :uid', array(':nid' => $node->nid, ':uid' => $user->uid))->fetchField();
        if (empty($poll->vote)) {
          $poll->vote = -1;
          $poll->allowvotes = TRUE;
        }
485
      }
486 487
      elseif (!empty($_SESSION['poll_vote'][$node->nid])) {
        $poll->vote = $_SESSION['poll_vote'][$node->nid];
488 489
      }
      else {
490
        $poll->allowvotes = !db_query("SELECT 1 FROM {poll_vote} WHERE nid = :nid AND hostname = :hostname", array(':nid' => $node->nid, ':hostname' => ip_address()))->fetchField();
491
      }
492
    }
493 494
    foreach ($poll as $key => $value) {
      $nodes[$node->nid]->$key = $value;
495 496
    }
  }
497
}
Dries's avatar
 
Dries committed
498

499
/**
500
 * Implements hook_insert().
501
 */
502
function poll_insert($node) {
503 504 505 506 507 508 509 510
  if (!user_access('administer nodes')) {
    // Make sure all votes are 0 initially
    foreach ($node->choice as $i => $choice) {
      $node->choice[$i]['chvotes'] = 0;
    }
    $node->active = 1;
  }

511 512 513 514 515 516 517
  db_insert('poll')
    ->fields(array(
      'nid' => $node->nid,
      'runtime' => $node->runtime,
      'active' => $node->active,
    ))
    ->execute();
518 519 520

  foreach ($node->choice as $choice) {
    if ($choice['chtext'] != '') {
521 522 523 524 525 526 527 528
      db_insert('poll_choice')
        ->fields(array(
          'nid' => $node->nid,
          'chtext' => $choice['chtext'],
          'chvotes' => $choice['chvotes'],
          'weight' => $choice['weight'],
        ))
        ->execute();
529 530 531 532 533
    }
  }
}

/**
534
 * Implements hook_update().
535
 */
536
function poll_update($node) {
537
  // Update poll settings.
538 539 540 541 542 543 544
  db_update('poll')
    ->fields(array(
      'runtime' => $node->runtime,
      'active' => $node->active,
    ))
    ->condition('nid', $node->nid)
    ->execute();
545

546 547 548 549
  // Poll choices with empty titles signifies removal. We remove all votes to
  // the removed options, so people who voted on them can vote again.
  foreach ($node->choice as $key => $choice) {
    if (!empty($choice['chtext'])) {
550 551 552 553 554 555 556 557 558 559
      db_merge('poll_choice')
        ->key(array('chid' => $choice['chid']))
        ->fields(array(
          'nid' => $node->nid,
          'chtext' => $choice['chtext'],
          'chvotes' => (int) $choice['chvotes'],
          'weight' => $choice['weight'],
        ))
        ->updateExcept('nid')
        ->execute();
560 561
    }
    else {
562 563 564 565
      db_delete('poll_vote')
        ->condition('nid', $node->nid)
        ->condition('chid', $key)
        ->execute();
566 567 568 569 570
    }
  }
}

/**
571
 * Implements hook_delete().
572
 */
573
function poll_delete($node) {
574 575 576 577 578 579 580 581
  db_delete('poll')
    ->condition('nid', $node->nid)
    ->execute();
  db_delete('poll_choice')
    ->condition('nid', $node->nid)
    ->execute();
  db_delete('poll_vote')
    ->condition('nid', $node->nid)
582
    ->execute();
583 584 585
}

/**
586
 * Return content for 'latest poll' block.
587
 *
588 589
 * @param $node
 *   The node object to load.
590
 */
591
function poll_block_latest_poll_view($node) {
592 593 594
  global $user;
  $output = '';

595 596 597 598 599 600 601 602 603 604 605 606
  // This is necessary for shared objects because PHP doesn't copy objects, but
  // passes them by reference.  So when the objects are cached it can result in
  // the wrong output being displayed on subsequent calls.  The cloning and
  // unsetting of $node->content prevents the block output from being the same
  // as the node output.
  $node = clone $node;
  unset($node->content);

  // No 'read more' link.
  $node->readmore = FALSE;
  $node->teaser = '';

607
  $links = array();
608 609 610 611
  $links[] = array('title' => t('Older polls'), 'href' => 'poll', 'attributes' => array('title' => t('View the list of polls on this site.')));
  if ($node->allowvotes) {
    $links[] = array('title' => t('Results'), 'href' => 'node/' . $node->nid . '/results', 'attributes' => array('title' => t('View the current poll results.')));
  }
612

613
  $node->links = $links;
614

615 616 617 618 619
  if (!empty($node->allowvotes)) {
    $node->content['poll_view_voting'] = drupal_get_form('poll_view_voting', $node, TRUE);
  }
  else {
    $node->content['poll_view_results'] = array('#markup' => poll_view_results($node, TRUE, TRUE));
620 621
  }

622 623 624 625 626
  return $node;
}


/**
627
 * Implements hook_view().
628
 */
629
function poll_view($node, $view_mode = 'full') {
630 631 632 633 634
  global $user;
  $output = '';

  if (!empty($node->allowvotes) && empty($node->show_results)) {
    $node->content['poll_view_voting'] = drupal_get_form('poll_view_voting', $node);
635 636
  }
  else {
637
    $node->content['poll_view_results'] = array('#markup' => poll_view_results($node, $view_mode));
638 639
  }
  return $node;
640
}
641

642
/**
643
 * Creates a simple teaser that lists all the choices.
644 645
 *
 * This is primarily used for RSS.
646
 */
647
function poll_teaser($node) {
648
  $teaser = NULL;
Dries's avatar
Dries committed
649
  if (is_array($node->choice)) {
Steven Wittens's avatar
Steven Wittens committed
650
    foreach ($node->choice as $k => $choice) {
651
      if ($choice['chtext'] != '') {
652
        $teaser .= '* ' . check_plain($choice['chtext']) . "\n";
653
      }
654 655 656 657
    }
  }
  return $teaser;
}
658

Steven Wittens's avatar
Steven Wittens committed
659
/**
660
 * Generates the voting form for a poll.
661 662 663 664
 *
 * @ingroup forms
 * @see poll_vote()
 * @see phptemplate_preprocess_poll_vote()
Steven Wittens's avatar
Steven Wittens committed
665
 */
666
function poll_view_voting($form, &$form_state, $node, $block = FALSE) {
Dries's avatar
 
Dries committed
667
  if ($node->choice) {
Steven Wittens's avatar
Steven Wittens committed
668 669
    $list = array();
    foreach ($node->choice as $i => $choice) {
670
      $list[$i] = check_plain($choice['chtext']);
671
    }
672 673 674 675 676
    $form['choice'] = array(
      '#type' => 'radios',
      '#default_value' => -1,
      '#options' => $list,
    );
Dries's avatar
 
Dries committed
677
  }
678

679
  $form['vote'] = array(
680
    '#type' => 'submit',
681 682 683 684 685 686 687 688 689 690
    '#value' => t('Vote'),
    '#submit' => array('poll_vote'),
  );

  // Store the node so we can get to it in submit functions.
  $form['#node'] = $node;
  $form['#block'] = $block;

  // Set form caching because we could have multiple of these forms on
  // the same page, and we want to ensure the right one gets picked.
691
  $form_state['cache'] = TRUE;
692 693 694

  // Provide a more cleanly named voting form theme.
  $form['#theme'] = 'poll_vote';
695
  return $form;
696
}
Steven Wittens's avatar
Steven Wittens committed
697

698 699 700 701 702 703 704 705 706
/**
 * Validation function for processing votes
 */
function poll_view_voting_validate($form, &$form_state) {
  if ($form_state['values']['choice'] == -1) {
    form_set_error( 'choice', t('Your vote could not be recorded because you did not select any of the choices.'));
  }
}

707
/**
708
 * Submit handler for processing a vote.
709 710 711 712 713 714
 */
function poll_vote($form, &$form_state) {
  $node = $form['#node'];
  $choice = $form_state['values']['choice'];

  global $user;
715 716 717 718 719
  db_insert('poll_vote')
    ->fields(array(
      'nid' => $node->nid,
      'chid' => $choice,
      'uid' => $user->uid,
720 721
      'hostname' => $user->uid ? '' : ip_address(),
      'timestamp' => REQUEST_TIME,
722 723
    ))
    ->execute();
724 725

  // Add one to the votes.
726 727 728 729
  db_update('poll_choice')
    ->expression('chvotes', 'chvotes + 1')
    ->condition('chid', $choice)
    ->execute();
730 731

  cache_clear_all();
732 733 734 735 736 737 738 739 740 741

  if (!$user->uid) {
    // The vote is recorded so the user gets the result view instead of the
    // voting form when viewing the poll. Saving a value in $_SESSION has the
    // convenient side effect of preventing the user from hitting the page
    // cache. When anonymous voting is allowed, the page cache should only
    // contain the voting form, not the results.
    $_SESSION['poll_vote'][$node->nid] = $choice;
  }

742 743 744 745 746
  drupal_set_message(t('Your vote was recorded.'));

  // Return the user to whatever page they voted from.
}

747 748
/**
 * Themes the voting form for a poll.
749 750
 *
 * Inputs: $form
751
 */
752 753 754
function template_preprocess_poll_vote(&$variables) {
  $form = $variables['form'];
  $variables['choice'] = drupal_render($form['choice']);
755
  $variables['title'] = check_plain($form['#node']->title);
756
  $variables['vote'] = drupal_render($form['vote']);
757
  $variables['rest'] = drupal_render_children($form);
758
  $variables['block'] = $form['#block'];
759
  if ($variables['block']) {
760
    $variables['theme_hook_suggestions'][] = 'poll_vote__block';
761
  }
Steven Wittens's avatar
Steven Wittens committed
762 763
}

764 765 766
/**
 * Generates a graphical representation of the results of a poll.
 */
767
function poll_view_results($node, $view_mode, $block = FALSE) {
Steven Wittens's avatar
Steven Wittens committed
768
  // Count the votes and find the maximum
769 770
  $total_votes = 0;
  $max_votes = 0;
Steven Wittens's avatar
Steven Wittens committed
771
  foreach ($node->choice as $choice) {
772 773 774 775
    if (isset($choice['chvotes'])) {
      $total_votes += $choice['chvotes'];
      $max_votes = max($max_votes, $choice['chvotes']);
    }
Steven Wittens's avatar
Steven Wittens committed
776 777
  }

778
  $poll_results = '';
Steven Wittens's avatar
Steven Wittens committed
779
  foreach ($node->choice as $i => $choice) {
780
    if (!empty($choice['chtext'])) {
781
      $chvotes = isset($choice['chvotes']) ? $choice['chvotes'] : NULL;
782
      $poll_results .= theme('poll_bar', array('title' => $choice['chtext'], 'votes' => $chvotes, 'total_votes' => $total_votes, 'vote' => isset($node->vote) && $node->vote == $i, 'block' => $block));
783
    }
784
  }
Steven Wittens's avatar
Steven Wittens committed
785

786
  return theme('poll_results', array('raw_title' => $node->title, 'results' => $poll_results, 'votes' => $total_votes, 'raw_links' => isset($node->links) ? $node->links : array(), 'block' => $block, 'nid' => $node->nid, 'vote' => isset($node->vote) ? $node->vote : NULL));
Steven Wittens's avatar
Steven Wittens committed
787 788
}

789 790

/**
791 792 793 794 795
 * Returns HTML for an admin poll form for choices.
 *
 * @param $variables
 *   An associative array containing:
 *   - form: A render element representing the form.
796 797
 *
 * @ingroup themeable
798
 */
799 800 801
function theme_poll_choices($variables) {
  $form = $variables['form'];

802
  drupal_add_tabledrag('poll-choice-table', 'order', 'sibling', 'poll-weight');
803

804
  $delta = 0;
805 806
  $rows = array();
  $headers = array(
807
    '',
808 809
    t('Choice'),
    t('Vote count'),
810
    t('Weight'),
811 812 813
  );

  foreach (element_children($form) as $key) {
814 815
    $delta++;
    // Set special classes for drag and drop updating.
816
    $form[$key]['weight']['#attributes']['class'] = array('poll-weight');
817 818 819 820

    // Build the table row.
    $row = array(
      'data' => array(
821
        array('class' => array('choice-flag')),
822 823 824
        drupal_render($form[$key]['chtext']),
        drupal_render($form[$key]['chvotes']),
        drupal_render($form[$key]['weight']),
825
      ),
826
      'class' => array('draggable'),
827 828
    );

829
    // Add any additional classes set on the row.
830 831 832
    if (!empty($form[$key]['#attributes']['class'])) {
      $row['class'] = array_merge($row['class'], $form[$key]['#attributes']['class']);
    }
833

834 835 836
    $rows[] = $row;
  }

837
  $output = theme('table', array('header' => $headers, 'rows' => $rows, 'attributes' => array('id' => 'poll-choice-table')));
838
  $output .= drupal_render_children($form);
839 840 841
  return $output;
}

Steven Wittens's avatar
Steven Wittens committed
842
/**
843 844 845 846 847 848 849 850 851
 * Preprocess the poll_results theme hook.
 *
 * Inputs: $raw_title, $results, $votes, $raw_links, $block, $nid, $vote. The
 * $raw_* inputs to this are naturally unsafe; often safe versions are
 * made to simply overwrite the raw version, but in this case it seems likely
 * that the title and the links may be overridden by the theme layer, so they
 * are left in with a different name for that purpose.
 *
 * @see poll-results.tpl.php
852
 * @see poll-results--block.tpl.php
Steven Wittens's avatar
Steven Wittens committed
853
 */
854
function template_preprocess_poll_results(&$variables) {
855
  $variables['links'] = theme('links__poll_results', array('links' => $variables['raw_links']));
856
  if (isset($variables['vote']) && $variables['vote'] > -1 && user_access('cancel own vote')) {
857 858
    $elements = drupal_get_form('poll_cancel_form', $variables['nid']);
    $variables['cancel_form'] = drupal_render($elements);
Steven Wittens's avatar
Steven Wittens committed
859
  }
860 861 862
  $variables['title'] = check_plain($variables['raw_title']);

  if ($variables['block']) {
863
    $variables['theme_hook_suggestions'][] = 'poll_results__block';
Steven Wittens's avatar
Steven Wittens committed
864 865 866
  }
}

867
/**
868 869 870 871 872
 * Preprocess the poll_bar theme hook.
 *
 * Inputs: $title, $votes, $total_votes, $voted, $block
 *
 * @see poll-bar.tpl.php
873
 * @see poll-bar--block.tpl.php
874
 * @see theme_poll_bar()
875
 */
876 877
function template_preprocess_poll_bar(&$variables) {
  if ($variables['block']) {
878
    $variables['theme_hook_suggestions'][] = 'poll_bar__block';