poll.module 29.2 KB
Newer Older
1
2
<?php

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

9
/**
10
 * Implements hook_help().
11
 */
12
13
function poll_help($path, $arg) {
  switch ($path) {
Steven Wittens's avatar
Steven Wittens committed
14
    case 'admin/help#poll':
15
16
      $output = '';
      $output .= '<h3>' . t('About') . '</h3>';
17
      $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>';
18
19
20
      $output .= '<h3>' . t('Uses') . '</h3>';
      $output .= '<dl>';
      $output .= '<dt>' . t('Creating a poll') . '</dt>';
21
      $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>';
22
      $output .= '<dt>' . t('Viewing polls') . '</dt>';
23
      $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>';
24
      $output .= '</dl>';
25
      return $output;
Steven Wittens's avatar
Steven Wittens committed
26
  }
27
28
}

29
/**
30
 * Implements hook_theme().
31
32
 */
function poll_theme() {
33
  $theme_hooks = array(
34
    'poll_vote' => array(
35
      'template' => 'poll-vote',
36
      'render element' => 'form',
37
    ),
38
    'poll_choices' => array(
39
      'render element' => 'form',
40
    ),
41
    'poll_results' => array(
42
      'template' => 'poll-results',
43
      'variables' => array('raw_title' => NULL, 'results' => NULL, 'votes' => NULL, 'raw_links' => NULL, 'block' => NULL, 'nid' => NULL, 'vote' => NULL),
44
    ),
45
  );
46

47
  return $theme_hooks;
48
49
}

50
/**
51
 * Implements hook_permission().
52
 */
53
function poll_permission() {
54
  $perms = array(
55
56
57
58
    'vote on polls' => array(
      'title' => t('Vote on polls'),
    ),
    'cancel own vote' => array(
59
      'title' => t('Cancel and change own votes'),
60
61
    ),
    'inspect all votes' => array(
62
      'title' => t('View details for all votes'),
63
    ),
64
65
66
  );

  return $perms;
67
68
69
}

/**
70
 * Implements hook_menu().
71
72
73
74
75
76
77
 */
function poll_menu() {
  $items['poll'] = array(
    'title' => 'Polls',
    'page callback' => 'poll_page',
    'access arguments' => array('access content'),
    'type' => MENU_SUGGESTED_ITEM,
78
    'file' => 'poll.pages.inc',
79
80
81
82
83
84
85
86
87
88
  );

  $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,
89
    'file' => 'poll.pages.inc',
90
91
92
93
94
95
96
97
98
99
  );

  $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,
100
    'file' => 'poll.pages.inc',
101
  );
102

103
104
105
106
107
108
  return $items;
}

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

113
/**
114
 * Implements hook_block_info().
115
 */
116
function poll_block_info() {
117
  $blocks['recent']['info'] = t('Most recent poll');
118
  $blocks['recent']['properties']['administrative'] = TRUE;
119
  return $blocks;
120
121
122
}

/**
123
 * Implements hook_block_view().
124
125
126
 *
 * Generates a block containing the latest poll.
 */
127
function poll_block_view($delta = '') {
Steven Wittens's avatar
Steven Wittens committed
128
  if (user_access('access content')) {
129
    // Retrieve the latest poll.
130
131
132
133
134
135
136
137
138
139
    $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();
140
141
142
    if ($record) {
      $poll = node_load($record->nid);
      if ($poll->nid) {
143
        $poll = poll_block_latest_poll_view($poll);
144
        $block['subject'] = t('Poll');
145
        $block['content'] = $poll->content;
146
        return $block;
Dries's avatar
   
Dries committed
147
      }
148
149
150
151
    }
  }
}

152
/**
153
 * Implements hook_cron().
154
155
156
 *
 * Closes polls that have exceeded their allowed runtime.
 */
157
function poll_cron() {
158
159
160
161
162
163
  $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
164
  }
165
166
}

167
/**
168
 * Implements hook_node_info().
169
 */
170
171
172
173
function poll_node_info() {
  return array(
    'poll' => array(
      'name' => t('Poll'),
174
      'base' => 'poll',
175
      '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.'),
176
177
178
179
      'title_label' => t('Question'),
      'has_body' => FALSE,
    )
  );
180
181
}

182
/**
183
 * Implements hook_field_extra_fields().
184
 */
185
186
function poll_field_extra_fields() {
  $extra['node']['poll'] = array(
187
188
189
190
191
192
193
194
195
196
197
    'form' => array(
      'choice_wrapper' => array(
        'label' => t('Poll choices'),
        'description' => t('Poll choices'),
        'weight' => -4,
      ),
      'settings' => array(
        'label' => t('Poll settings'),
        'description' => t('Poll module settings'),
        'weight' => -3,
      ),
198
    ),
199
200
201
202
203
204
205
206
207
208
209
210
    'display' => array(
      'poll_view_voting' => array(
        'label' => t('Poll vote'),
        'description' => t('Poll vote'),
        'weight' => 0,
      ),
      'poll_view_results' => array(
        'label' => t('Poll results'),
        'description' => t('Poll results'),
        'weight' => 0,
      ),
    )
211
  );
212
213
214
215

  return $extra;
}

216
/**
217
 * Implements hook_form().
218
 */
219
function poll_form($node, &$form_state) {
220
221
  global $user;

222
  $admin = user_access('bypass node access') || user_access('edit any poll content') || (user_access('edit own poll content') && $user->uid == $node->uid);
223

224
  $type = node_type_get_type($node);
225

226
  // The submit handlers to add more poll choices require that this form is
227
  // cached, regardless of whether Ajax is used.
228
  $form_state['cache'] = TRUE;
229

230
231
232
233
234
235
236
237
  $form['title'] = array(
    '#type' => 'textfield',
    '#title' => check_plain($type->title_label),
    '#required' => TRUE,
    '#default_value' => $node->title,
    '#weight' => -5,
  );

238
239
  if (isset($form_state['choice_count'])) {
    $choice_count = $form_state['choice_count'];
240
241
  }
  else {
242
    $choice_count = max(2, empty($node->choice) ? 2 : count($node->choice));
243
244
  }

245
246
247
  // Add a wrapper for the choices and more button.
  $form['choice_wrapper'] = array(
    '#tree' => FALSE,
248
    '#weight' => -4,
249
    '#prefix' => '<div class="clearfix" id="poll-choice-wrapper">',
250
    '#suffix' => '</div>',
251
  );
252

253
254
255
  // Container for just the poll choices.
  $form['choice_wrapper']['choice'] = array(
    '#prefix' => '<div id="poll-choices">',
256
    '#suffix' => '</div>',
257
    '#theme' => 'poll_choices',
258
259
  );

260
  // Add the current choices to the form.
261
262
263
264
265
  $delta = 0;
  $weight = 0;
  if (isset($node->choice)) {
    $delta = count($node->choice);
    foreach ($node->choice as $chid => $choice) {
266
      $key = 'chid:' . $chid;
267
      $form['choice_wrapper']['choice'][$key] = _poll_choice_form($key, $choice['chid'], $choice['chtext'], $choice['chvotes'], $choice['weight'], $choice_count);
268
      $weight = max($choice['weight'], $weight);
269
270
    }
  }
271

272
273
274
  // Add initial or additional choices.
  $existing_delta = $delta;
  for ($delta; $delta < $choice_count; $delta++) {
275
    $key = 'new:' . ($delta - $existing_delta);
276
277
    // Increase the weight of each new choice.
    $weight++;
278
    $form['choice_wrapper']['choice'][$key] = _poll_choice_form($key, NULL, '', 0, $weight, $choice_count);
279
280
281
  }

  // We name our button 'poll_more' to avoid conflicts with other modules using
282
  // Ajax-enabled buttons with the id 'more'.
283
284
  $form['choice_wrapper']['poll_more'] = array(
    '#type' => 'submit',
285
    '#value' => t('Add another choice'),
286
    '#weight' => 1,
287
288
    '#limit_validation_errors' => array(array('choice')),
    '#submit' => array('poll_more_choices_submit'),
289
    '#ajax' => array(
290
      'callback' => 'poll_choice_js',
291
292
293
      'wrapper' => 'poll-choices',
      'effect' => 'fade',
    ),
294
295
  );

Steven Wittens's avatar
Steven Wittens committed
296
  // Poll attributes
297
298
299
300
301
302
303
304
305
306
307
  $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');
308
309
310
311
312
313
314
315
316
  $active = array(0 => t('Closed'), 1 => t('Active'));

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

318
319
320
321
322
323
324
325
  $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,
  );
326
327
328
  $form['settings']['runtime'] = array(
    '#type' => 'select',
    '#title' => t('Poll duration'),
329
    '#default_value' => isset($node->runtime) ? $node->runtime : 0,
330
    '#options' => $duration,
331
332
    '#description' => t('After this period, the poll will be closed automatically.'),
  );
Dries's avatar
   
Dries committed
333

334
  return $form;
335
}
336

337
/**
338
339
340
341
342
343
344
 * 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.
345
346
 */
function poll_more_choices_submit($form, &$form_state) {
347
  // Add one more choice to the form.
348
  if ($form_state['values']['poll_more']) {
349
    $form_state['choice_count'] = count($form_state['values']['choice']) + 1;
350
  }
351
352
353
354
355
356
357
  // Renumber the choices. This invalidates the corresponding key/value
  // associations in $form_state['input'], so clear that out. This requires
  // poll_form() to rebuild the choices with the values in
  // $form_state['node']->choice, which it does.
  $form_state['node']->choice = array_values($form_state['values']['choice']);
  unset($form_state['input']['choice']);
  $form_state['rebuild'] = TRUE;
358
359
}

360
function _poll_choice_form($key, $chid = NULL, $value = '', $votes = 0, $weight = 0, $size = 10) {
361
362
  $form = array(
    '#tree' => TRUE,
363
    '#weight' => $weight,
364
365
366
367
  );

  // We'll manually set the #parents property of these fields so that
  // their values appear in the $form_state['values']['choice'] array.
368
369
370
371
372
373
  $form['chid'] = array(
    '#type' => 'value',
    '#value' => $chid,
    '#parents' => array('choice', $key, 'chid'),
  );

374
375
  $form['chtext'] = array(
    '#type' => 'textfield',
376
377
    '#title' => $value !== '' ? t('Choice label') : t('New choice label'),
    '#title_display' => 'invisible',
378
    '#default_value' => $value,
379
    '#parents' => array('choice', $key, 'chtext'),
380
381
  );

382
383
  $form['chvotes'] = array(
    '#type' => 'textfield',
384
385
    '#title' => $value !== '' ? t('Vote count for choice @label', array('@label' => $value)) : t('Vote count for new choice'),
    '#title_display' => 'invisible',
386
387
388
389
390
    '#default_value' => $votes,
    '#size' => 5,
    '#maxlength' => 7,
    '#parents' => array('choice', $key, 'chvotes'),
    '#access' => user_access('administer nodes'),
391
    '#element_validate' => array('element_validate_integer'),
392
393
394
395
  );

  $form['weight'] = array(
    '#type' => 'weight',
396
397
    '#title' => $value !== '' ? t('Weight for choice @label', array('@label' => $value)) : t('Weight for new choice'),
    '#title_display' => 'invisible',
398
399
400
401
    '#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
420
421
422
 * Form submit handler for node_form().
 *
 * Upon preview and final submission, we need to renumber poll choices and
 * create a teaser output.
423
424
 */
function poll_node_form_submit(&$form, &$form_state) {
425
  // Renumber choices.
426
  $form_state['values']['choice'] = array_values($form_state['values']['choice']);
427
  $form_state['values']['teaser'] = poll_teaser((object) $form_state['values']);
428
429
}

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

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

452
/**
453
 * Implements hook_field_attach_prepare_translation_alter().
454
 */
455
456
457
function poll_field_attach_prepare_translation_alter(&$entity, $context) {
  if ($context['entity_type'] == 'node' && $entity->type == 'poll') {
    $entity->choice = $context['source_entity']->choice;
458
459
460
    foreach ($entity->choice as $i => $choice) {
      $entity->choice[$i]['chvotes'] = 0;
    }
461
462
463
  }
}

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

472
    if (empty($poll)) {
473
      $poll = new stdClass();
474
475
    }

476
    // Load the appropriate choices into the $poll object.
477
478
479
    $poll->choice = db_select('poll_choice', 'c')
      ->addTag('translatable')
      ->fields('c', array('chid', 'chtext', 'chvotes', 'weight'))
480
      ->condition('c.nid', $node->nid)
481
      ->orderBy('weight')
482
      ->execute()->fetchAllAssoc('chid', PDO::FETCH_ASSOC);
483

484
485
486
487
    // Determine whether or not this user is allowed to vote.
    $poll->allowvotes = FALSE;
    if (user_access('vote on polls') && $poll->active) {
      if ($user->uid) {
488
        // If authenticated, find existing vote based on uid.
489
490
491
492
493
        $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;
        }
494
      }
495
      elseif (!empty($_SESSION['poll_vote'][$node->nid])) {
496
497
        // Otherwise the user is anonymous. Look for an existing vote in the
        // user's session.
498
        $poll->vote = $_SESSION['poll_vote'][$node->nid];
499
500
      }
      else {
501
502
503
        // Finally, query the database for an existing vote based on anonymous
        // user's hostname.
        $poll->allowvotes = !db_query("SELECT 1 FROM {poll_vote} WHERE nid = :nid AND hostname = :hostname AND uid = 0", array(':nid' => $node->nid, ':hostname' => ip_address()))->fetchField();
504
      }
505
    }
506
507
    foreach ($poll as $key => $value) {
      $nodes[$node->nid]->$key = $value;
508
509
    }
  }
510
}
Dries's avatar
   
Dries committed
511

512
/**
513
 * Implements hook_insert().
514
 */
515
function poll_insert($node) {
516
517
518
519
520
521
522
523
  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;
  }

524
525
526
527
528
529
530
  db_insert('poll')
    ->fields(array(
      'nid' => $node->nid,
      'runtime' => $node->runtime,
      'active' => $node->active,
    ))
    ->execute();
531
532
533

  foreach ($node->choice as $choice) {
    if ($choice['chtext'] != '') {
534
535
536
537
538
539
540
541
      db_insert('poll_choice')
        ->fields(array(
          'nid' => $node->nid,
          'chtext' => $choice['chtext'],
          'chvotes' => $choice['chvotes'],
          'weight' => $choice['weight'],
        ))
        ->execute();
542
543
544
545
546
    }
  }
}

/**
547
 * Implements hook_update().
548
 */
549
function poll_update($node) {
550
  // Update poll settings.
551
552
553
554
555
556
557
  db_update('poll')
    ->fields(array(
      'runtime' => $node->runtime,
      'active' => $node->active,
    ))
    ->condition('nid', $node->nid)
    ->execute();
558

559
560
561
562
  // 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'])) {
563
564
565
566
567
568
569
      db_merge('poll_choice')
        ->key(array('chid' => $choice['chid']))
        ->fields(array(
          'chtext' => $choice['chtext'],
          'chvotes' => (int) $choice['chvotes'],
          'weight' => $choice['weight'],
        ))
570
        ->insertFields(array(
571
572
573
574
          'nid' => $node->nid,
          'chtext' => $choice['chtext'],
          'chvotes' => (int) $choice['chvotes'],
          'weight' => $choice['weight'],
575
        ))
576
        ->execute();
577
578
    }
    else {
579
580
581
582
      db_delete('poll_vote')
        ->condition('nid', $node->nid)
        ->condition('chid', $key)
        ->execute();
583
584
585
586
      db_delete('poll_choice')
        ->condition('nid', $node->nid)
        ->condition('chid', $choice['chid'])
        ->execute();
587
588
589
590
591
    }
  }
}

/**
592
 * Implements hook_delete().
593
 */
594
function poll_delete($node) {
595
596
597
598
599
600
601
602
  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)
603
    ->execute();
604
605
606
}

/**
607
 * Return content for 'latest poll' block.
608
 *
609
610
 * @param $node
 *   The node object to load.
611
 */
612
function poll_block_latest_poll_view($node) {
613
614
615
  global $user;
  $output = '';

616
617
618
619
620
621
622
623
624
625
626
627
  // 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 = '';

628
  $links = array();
629
630
631
632
  $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.')));
  }
633

634
  $node->links = $links;
635

636
637
  if (!empty($node->allowvotes)) {
    $node->content['poll_view_voting'] = drupal_get_form('poll_view_voting', $node, TRUE);
638
639
640
641
642
    $node->content['links'] = array(
      '#theme' => 'links',
      '#links' => $node->links,
      '#weight' => 5,
    );
643
644
645
  }
  else {
    $node->content['poll_view_results'] = array('#markup' => poll_view_results($node, TRUE, TRUE));
646
647
  }

648
649
650
651
652
  return $node;
}


/**
653
 * Implements hook_view().
654
 */
655
function poll_view($node, $view_mode) {
656
657
658
659
660
  global $user;
  $output = '';

  if (!empty($node->allowvotes) && empty($node->show_results)) {
    $node->content['poll_view_voting'] = drupal_get_form('poll_view_voting', $node);
661
662
  }
  else {
663
    $node->content['poll_view_results'] = array('#markup' => poll_view_results($node, $view_mode));
664
665
  }
  return $node;
666
}
667

668
/**
669
 * Creates a simple teaser that lists all the choices.
670
671
 *
 * This is primarily used for RSS.
672
 */
673
function poll_teaser($node) {
674
  $teaser = NULL;
Dries's avatar
Dries committed
675
  if (is_array($node->choice)) {
Steven Wittens's avatar
Steven Wittens committed
676
    foreach ($node->choice as $k => $choice) {
677
      if ($choice['chtext'] != '') {
678
        $teaser .= '* ' . check_plain($choice['chtext']) . "\n";
679
      }
680
681
682
683
    }
  }
  return $teaser;
}
684

Steven Wittens's avatar
Steven Wittens committed
685
/**
686
 * Generates the voting form for a poll.
687
688
689
690
 *
 * @ingroup forms
 * @see poll_vote()
 * @see phptemplate_preprocess_poll_vote()
Steven Wittens's avatar
Steven Wittens committed
691
 */
692
function poll_view_voting($form, &$form_state, $node, $block = FALSE) {
Dries's avatar
   
Dries committed
693
  if ($node->choice) {
Steven Wittens's avatar
Steven Wittens committed
694
695
    $list = array();
    foreach ($node->choice as $i => $choice) {
696
      $list[$i] = check_plain($choice['chtext']);
697
    }
698
699
    $form['choice'] = array(
      '#type' => 'radios',
700
701
      '#title' => t('Choices'),
      '#title_display' => 'invisible',
702
703
704
      '#default_value' => -1,
      '#options' => $list,
    );
Dries's avatar
   
Dries committed
705
  }
706

707
  $form['vote'] = array(
708
    '#type' => 'submit',
709
710
711
712
713
714
715
716
717
718
    '#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.
719
  $form_state['cache'] = TRUE;
720
721
722

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

726
727
728
729
730
731
732
733
734
/**
 * 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.'));
  }
}

735
/**
736
 * Submit handler for processing a vote.
737
738
739
740
741
742
 */
function poll_vote($form, &$form_state) {
  $node = $form['#node'];
  $choice = $form_state['values']['choice'];

  global $user;
743
744
745
746
747
  db_insert('poll_vote')
    ->fields(array(
      'nid' => $node->nid,
      'chid' => $choice,
      'uid' => $user->uid,
748
      'hostname' => ip_address(),
749
      'timestamp' => REQUEST_TIME,
750
751
    ))
    ->execute();
752
753

  // Add one to the votes.
754
755
756
757
  db_update('poll_choice')
    ->expression('chvotes', 'chvotes + 1')
    ->condition('chid', $choice)
    ->execute();
758
759

  cache_clear_all();
760
761
762
763
764
765
766
767
768
769

  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;
  }

770
771
772
773
774
  drupal_set_message(t('Your vote was recorded.'));

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

775
776
777
778
779
780
781
782
783
/**
 * Implements hook_preprocess_block().
 */
function poll_preprocess_block(&$variables) {
  if ($variables['block']->module == 'poll') {
    $variables['attributes_array']['role'] = 'complementary';
  }
}

784
785
/**
 * Themes the voting form for a poll.
786
787
 *
 * Inputs: $form
788
 */
789
790
791
function template_preprocess_poll_vote(&$variables) {
  $form = $variables['form'];
  $variables['choice'] = drupal_render($form['choice']);
792
  $variables['title'] = check_plain($form['#node']->title);
793
  $variables['vote'] = drupal_render($form['vote']);
794
  $variables['rest'] = drupal_render_children($form);
795
  $variables['block'] = $form['#block'];
796
  if ($variables['block']) {
797
    $variables['theme_hook_suggestions'][] = 'poll_vote__block';
798
  }
Steven Wittens's avatar
Steven Wittens committed
799
800
}

801
802
803
/**
 * Generates a graphical representation of the results of a poll.
 */
804
function poll_view_results($node, $view_mode, $block = FALSE) {
805
806
807
  // Make sure that choices are ordered by their weight.
  uasort($node->choice, 'drupal_sort_weight');

Steven Wittens's avatar
Steven Wittens committed
808
  // Count the votes and find the maximum
809
810
  $total_votes = 0;
  $max_votes = 0;
Steven Wittens's avatar
Steven Wittens committed
811
  foreach ($node->choice as $choice) {
812
813
814
815
    if (isset($choice['chvotes'])) {
      $total_votes += $choice['chvotes'];
      $max_votes = max($max_votes, $choice['chvotes']);
    }
Steven Wittens's avatar
Steven Wittens committed
816
817
  }

818
  $poll_results = array();
Steven Wittens's avatar
Steven Wittens committed
819
  foreach ($node->choice as $i => $choice) {
820
821
822
823
824
825
    $chvotes = isset($choice['chvotes']) ? $choice['chvotes'] : NULL;
    $percentage = round($chvotes * 100 / max($total_votes, 1));
    $display_votes = !$block ? ' (' . format_plural($chvotes, '1 vote', '@count votes') . ')' : '';

    $poll_results[] = array(
      '#theme' => 'meter',
826
827
      '#prefix' => '<dt class="choice-title">' . check_plain($choice['chtext']) . '</dt>' . PHP_EOL . '<dd class="choice-result">',
      '#suffix' => '</dd>' . PHP_EOL,
828
829
830
831
832
833
834
      '#display_value' =>  t('!percentage%', array('!percentage' => $percentage)) . $display_votes,
      '#min' => 0,
      '#max' => $total_votes,
      '#value' => $chvotes,
      '#percentage' => $percentage,
      '#attributes' => array('class' => 'bar'),
    );
835
  }
Steven Wittens's avatar
Steven Wittens committed
836

837
  return theme('poll_results', array('raw_title' => $node->title, 'results' => drupal_render($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
838
839
}

840
841

/**
842
843
844
845
846
 * Returns HTML for an admin poll form for choices.
 *
 * @param $variables
 *   An associative array containing:
 *   - form: A render element representing the form.
847
848
 *
 * @ingroup themeable
849
 */
850
851
852
function theme_poll_choices($variables) {
  $form = $variables['form'];

853
  drupal_add_tabledrag('poll-choice-table', 'order', 'sibling', 'poll-weight');
854

855
  $is_admin= user_access('administer nodes');
856
  $delta = 0;
857
  $rows = array();
858
859
860
861
862
  $headers = array('', t('Choice'));
  if ($is_admin) {
    $headers[] = t('Vote count');
  }
  $headers[] = t('Weight');
863
864

  foreach (element_children($form) as $key) {
865
866
    $delta++;
    // Set special classes for drag and drop updating.
867
    $form[$key]['weight']['#attributes']['class'] = array('poll-weight');
868
869
870
871

    // Build the table row.
    $row = array(
      'data' => array(
872
        array('class' => array('choice-flag')),
873
        drupal_render($form[$key]['chtext']),
874
      ),
875
      'class' => array('draggable'),
876
    );
877
878
879
880
    if ($is_admin) {
      $row['data'][] = drupal_render($form[$key]['chvotes']);
    }
    $row['data'][] = drupal_render($form[$key]['weight']);
881

882
    // Add any additional classes set on the row.
883
884
885
    if (!empty($form[$key]['#attributes']['class'])) {
      $row['class'] = array_merge($row['class'], $form[$key]['#attributes']['class']);
    }
886

887
888
889
    $rows[] = $row;
  }

890
  $output = theme('table', array('header' => $headers, 'rows' => $rows, 'attributes' => array('id' => 'poll-choice-table')));
891
  $output .= drupal_render_children($form);
892
893
894
  return $output;
}

Steven Wittens's avatar
Steven Wittens committed
895
/**
896
897
898
899
900
901
902
903
904
 * 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
905
 * @see poll-results--block.tpl.php
Steven Wittens's avatar
Steven Wittens committed
906
 */
907
function template_preprocess_poll_results(&$variables) {
908
  $variables['links'] = theme('links__poll_results', array('links' => $variables['raw_links']));
909
  if (isset($variables['vote']) && $variables['vote'] > -1 && user_access('cancel own vote')) {
910
911
    $elements = drupal_get_form('poll_cancel_form', $variables['nid']);
    $variables['cancel_form'] = drupal_render($elements);
Steven Wittens's avatar
Steven Wittens committed
912
  }
913
  $variables['title'] = check_plain($variables['raw_title']);
914
915
}

Steven Wittens's avatar
Steven Wittens committed
916
/**
917
918
919
920
 * Builds the cancel form for a poll.
 *
 * @ingroup forms
 * @see poll_cancel()
Steven Wittens's avatar
Steven Wittens committed
921
 */
922
function poll_cancel_form($form, &$form_state, $nid) {
923
924
  $form_state['cache'] = TRUE;

925
926
  // Store the nid so we can get to it in submit functions.
  $form['#nid'] = $nid;
927

928
929
  $form['actions'] = array('#type' => 'actions');
  $form['actions']['submit'] = array(
930
    '#type' => 'submit',
Dries's avatar