/[drupal]/contributions/modules/decisions/decisions.module
ViewVC logotype

Contents of /contributions/modules/decisions/decisions.module

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.222 - (show annotations) (download) (as text)
Mon Oct 26 20:41:27 2009 UTC (4 weeks, 1 day ago) by anarcat
Branch: MAIN
CVS Tags: HEAD
Changes since 1.221: +32 -3 lines
File MIME type: text/x-php
#574178 by wonder95 - add option to display options in random order
1 <?php
2
3 /**
4 * @file
5 *
6 * Modular voting mechanisms, delegatable votes, taxonomy/category
7 * influenced controls and weighted voting
8 *
9 * See http://decisions.gnuvernment.org for more information on the project.
10 *
11 * Heavily inspired by other Drupal modules, mostly from poll.module,
12 * but we adapted it to "drupal forms api". Thanks to everyone for
13 * all the that was already written. (...and debugged!)
14 */
15
16 // $Id: decisions.module,v 1.221 2009/08/19 22:34:36 anarcat Exp $
17
18 define('DECISIONS_DEFAULT_ELECTORAL_LIST', 0);
19 // always, aftervote, or afterclose
20 define('DECISIONS_DEFAULT_VIEW_RESULTS', 'aftervote');
21 define('DECISIONS_RUNTIME_INFINITY', 0);
22
23
24 /**
25 * hook_init() implementation
26 *
27 * Most of the stuff here is in subfiles now.
28 *
29 * Decision modes are in seperate modules in modes/*.module
30 */
31 function decisions_init() {
32 // extension files are included here in order to lighten Drupal bootstrap
33 drupal_add_css(drupal_get_path('module', 'poll') .'/poll.css');
34 }
35
36 /**
37 * Implementation of hook_access().
38 */
39 function decisions_access($op, $node, $account) {
40 if ($op == 'create') {
41 return user_access('create decisions', $account);
42 }
43 if ($op == 'delete') {
44 return user_access('delete decisions', $account);
45 }
46 if ($op == 'update') {
47 /* you can update it if you can create it, provided it is your own... */
48 if (user_access('create decisions', $account) && ($account->uid == $node->uid)) {
49 return TRUE;
50 }
51 }
52 }
53
54 /**
55 * Implementation of hook_block().
56 */
57 function decisions_block($op = 'list', $delta = 'mostrecent', $edit = array()) {
58 if ($op == 'list') {
59 $block = array('mostrecent' => array('info' => t('Decisions - Newest')));
60 }
61 elseif ($op == 'view') {
62 if (user_access('view decisions')) {
63 switch ($delta) {
64 case 'mostrecent':
65 $block = array('subject' => t('Decisions - Newest'), 'content' => _decisions_block_mostrecent());
66 break;
67 default:
68 $block = array();
69 break;
70 }
71 }
72 }
73 return $block;
74 }
75
76 /**
77 * Implementation of hook_cron().
78 *
79 * Closes decisions that have exceeded their allowed runtime.
80 */
81 function decisions_cron() {
82 $result = db_query('SELECT d.nid FROM {decisions} d INNER JOIN {node} n ON d.nid = n.nid WHERE (d.startdate + d.runtime) < '. time() .' AND d.active = 1 AND d.runtime <> 0');
83 while ($decision = db_fetch_object($result)) {
84 db_query("UPDATE {decisions} SET active = 0 WHERE nid=%d", $decision->nid);
85 }
86 }
87
88 /**
89 * Implementation of votingapi_hook_calculate()
90 */
91 function decisions_votingapi_calculate(&$cache, $votes, $content_type, $content_id) {
92 if ($content_type == 'decisions') {
93 $node = node_load($content_id);
94 $mode = _decisions_get_mode($node);
95 $function = "{$mode}_decisions_votingapi_calculate";
96 if (function_exists($function)) {
97 return call_user_func($function, $node, $cache, $votes, $content_type, $content_id);
98 }
99 }
100 }
101
102 /**
103 * Implementation of hook_help().
104 */
105 function decisions_help($path, $arg) {
106 switch ($path) {
107 case 'admin/modules#description':
108 return t('Allow people to reproduce and surpass the kinds of decision-making instances that exist in face-to-face meetings.');
109 }
110 }
111
112 /**
113 * Implementation of hook_menu().
114 *
115 * Just a path for creating new decisions for now, but we could
116 * eventually have a 'my decisions' and 'view decisions' kind of
117 * page. (TODO)
118 */
119 function decisions_menu() {
120 $items['admin/settings/decisions'] = array(
121 'title' => 'Configure decisions',
122 'description' => 'Configure Decisions',
123 'page callback' => 'drupal_get_form',
124 'page arguments' => array('decisions_admin'),
125 'access arguments' => array('administer decisions'),
126 'type' => MENU_NORMAL_ITEM,
127 );
128
129 $items['node/%node/votes'] = array(
130 'title' => 'Votes',
131 'page callback' => 'decisions_votes_tab',
132 'page arguments' => array(1),
133 'access callback' => '_decisions_votes_access',
134 'access arguments' => array(1, 'inspect all votes'),
135 'weight' => 4,
136 'type' => MENU_LOCAL_TASK
137 );
138 $items['node/%node/results'] = array(
139 'title' => 'Results',
140 'page callback' => 'decisions_results',
141 'page arguments' => array(1),
142 'access callback' => '_decisions_can_view_results',
143 'access arguments' => array(1),
144 'weight' => 1,
145 'type' => MENU_LOCAL_TASK);
146 $items['node/%node/electoral_list'] = array(
147 'title' => 'Electoral list',
148 'page callback' => 'decisions_electoral_list_tab',
149 'page arguments' => array(1),
150 'access callback' => '_decisions_electoral_list_access',
151 'access arguments' => array(1, 'view electoral list'),
152 'weight' => 2,
153 'type' => MENU_LOCAL_TASK
154 );
155 // Allow voters to be removed
156 $items['node/%node/remove'] = array(
157 'page callback' => 'decisions_electoral_list_remove_voter',
158 'page arguments' => array(1, 3),
159 'access callback' => '_decisions_electoral_list_access',
160 'access arguments' => array(1, 'remove voters'),
161 'weight' => 3,
162 'type' => MENU_CALLBACK,
163 );
164 $items['node/%node/reset'] = array(
165 'title' => 'Reset votes',
166 'page callback' => 'drupal_get_form',
167 'page arguments' => array('decisions_reset_form', 1),
168 'access callback' => '_decisions_reset_access',
169 'access arguments' => array(1, 'administer decisions'),
170 'weight' => 3,
171 'type' => MENU_LOCAL_TASK,
172 );
173 $items['decisions/add_choices_js'] = array(
174 'page callback' => 'decisions_add_choices_js',
175 'type' => MENU_CALLBACK,
176 'access arguments' => array('create decisions'),
177 );
178
179 return $items;
180 }
181
182 /**
183 * Implementation of hook_perm().
184 */
185 function decisions_perm() {
186 return array('create decisions', 'delete decisions', 'view decisions', 'vote on decisions', 'cancel own vote', 'administer decisions', 'inspect all votes', 'view electoral list', 'remove voters');
187 }
188
189 /**
190 * Implementation of the admin_settings hook
191 */
192 function decisions_admin() {
193
194 $enabled = array(0 => t('Disabled'), 1 => t('Enabled'));
195
196 $form['main']['decisions_default_electoral_list'] = array(
197 '#type' => 'radios',
198 '#title' => t('Use electoral list by default'),
199 '#description' => t('Use an electoral list by default for new decisions.'),
200 '#default_value' => variable_get('decisions_default_electoral_list', DECISIONS_DEFAULT_ELECTORAL_LIST),
201 '#options' => $enabled,
202 );
203
204 $view_results = array(
205 'always' => t('Always'),
206 'aftervote' => t('After user has voted'),
207 'afterclose' => t('After voting has closed'),
208 );
209
210 $form['main']['decisions_view_results'] = array(
211 '#type' => 'radios',
212 '#title' => t('When should results be displayed'),
213 '#description' => t('Determines when users may view the results of the decision.'),
214 '#default_value' => variable_get('decisions_view_results', DECISIONS_DEFAULT_VIEW_RESULTS),
215 '#options' => $view_results,
216 );
217
218 return system_settings_form($form);
219 }
220
221 function decisions_cancel_form($form_state, $nid) {
222 $form['node'] = array('#type' => 'hidden', '#value' => $nid);
223 $form['submit'] = array('#type' => 'submit', '#value' => t('Cancel your vote'));
224 return $form;
225 }
226
227 function decisions_cancel_form_submit($form, &$form_state) {
228 decisions_cancel($form_state['values']['node']);
229 }
230
231 /*******************/
232 /* Theme functions */
233 /*******************/
234
235 function decisions_theme($existing, $type, $theme, $path) {
236 return array(
237 'decisions_view_header' => array('arguments' => array('node' => NULL, 'teaser' => FALSE)),
238 'decisions_view_voting' => array('arguments' => array('form' => NULL)),
239 'decisions_bar' => array('arguments' => array('title' => NULL, 'percentage' => NULL, 'votes' => NULL)),
240 'decisions_status' => array('arguments' => array('message' => NULL)),
241 'decisions_morechoices' => array('arguments' => array(), 'decisions_morechoices' => NULL),
242 'decisions_view_own_result' => array(),
243 );
244 }
245
246 function theme_decisions_view_own_result() {
247 $output = '<div class="decisions-own-result">' . t("Your vote has been recorded.") . '</div>';
248 return $output;
249 }
250
251 /**
252 * Theme stub for rendering ecisions header (contains dates and quorum informations).
253 */
254 function theme_decisions_view_header($node, $teaser = FALSE) {
255
256 $output = '<div class="decisions-header">';
257
258 // dates
259 $output .= '<div class="decisions-dates">';
260 $output .= theme('item_list',
261 array(
262 t('Current date: @date', array('@date' => format_date(time()))),
263 t('Opening date: @date', array('@date' => format_date($node->startdate))),
264 ($node->runtime == DECISIONS_RUNTIME_INFINITY ?
265 t('No closing date.') :
266 t('Closing date: @date', array('@date' => format_date($node->startdate + $node->runtime))))));
267 $output .= '</div>';
268
269 // votes
270 $num_eligible_voters = _decisions_count_eligible($node);
271 $num_voters = _decisions_count_voters($node);
272 $output .= '<div class="decisions-votes">';
273 $output .= t('@num-voters out of @num-voters-eligible eligible @voters cast their ballot',
274 array(
275 '@num-voters' => $num_voters,
276 '@num-voters-eligible' => $num_eligible_voters,
277 '@voters' => format_plural($num_eligible_voters, 'voter', 'voters')
278 )
279 );
280 $output .= '</div>';
281
282 // quorum
283 $quorum = _decisions_get_quorum($node);
284 if ($quorum > 0) {
285 $output .= '<div class="decisions-quorum">';
286 $output .= t('Quorum: @d', array('@d' => $quorum));
287 $output .= '</div>';
288 }
289
290 $output .= '</div>';
291 return $output;
292 }
293
294 /**
295 * Theme stub for redering the voting form, to allow the chance for
296 * themes to make this nicer/different
297 */
298 function theme_decisions_view_voting($form) {
299
300 $render = 'drupal_render';
301 if (!function_exists($render)) {
302 $render = 'form_render';
303 }
304 $output .= '<div class="decisions">';
305 $output .= ' <div class="choice-form">';
306 $output .= ' <div class="choices">';
307 $output .= $render($form['choice']);
308 $output .= ' </div>';
309 $output .= $render($form['nid']);
310 $output .= $render($form['vote']);
311 $output .= ' </div>';
312 $output .= $render($form);
313 $output .= '</div>';
314 return $output;
315 }
316
317 /**
318 * Theme stub for a decisions bar.
319 */
320 function theme_decisions_bar($title, $percentage, $votes) {
321 $output = '<div class="text">'. $title .'</div>';
322 $output .= '<div class="bar"><div style="width: '. $percentage .'%;" class="foreground"></div></div>';
323 $output .= '<div class="percent">'. $percentage .'% ('. $votes .')</div>';
324 return $output;
325 }
326
327 /**
328 * Outputs a status line.
329 */
330 function theme_decisions_status($message) {
331 return '<div class="error">'. $message .'</div>';
332 }
333
334
335 /****************************/
336 /* Electoral list functions */
337 /****************************/
338
339 /**
340 * Creates the form for the electoral list.
341 */
342 function decisions_electoral_list_form($form_state, $nid) {
343 $form = array();
344 $form['electoral_list'] = array(
345 '#type' => 'fieldset',
346 '#tree' => TRUE,
347 '#title' => t('Administer electoral list'),
348 '#collapsible' => TRUE,
349 '#weight' => 2,
350 '#collapsed' => TRUE,
351 );
352
353 $form['electoral_list']['add_user'] = array(
354 '#type' => 'textfield',
355 '#title' => t('Add user'),
356 '#size' => 40,
357 '#autocomplete_path' => 'user/autocomplete',
358 '#description' => t('Add an individual user to the electoral list'),
359 );
360
361 $form['electoral_list']['submit'] = array(
362 '#type' => 'submit',
363 '#value' => t('Modify electoral list'),
364 );
365
366 $form['electoral_list']['reset'] = array(
367 '#type' => 'button',
368 '#value' => t('Reset electoral list'),
369 );
370
371 $form['nid'] = array('#type' => 'hidden', '#value' => $nid);
372 return $form;
373
374 }
375
376 /**
377 * Outputs the electoral list tab.
378 */
379 function decisions_electoral_list_tab() {
380 if ($node = menu_get_object()) {
381 $output = "";
382 if (!$node->uselist) {
383 drupal_not_found();
384 return;
385 }
386 drupal_set_title(check_plain($node->title));
387 if (user_access('administer decisions')) {
388 $form['electoral_list'] = array(
389 '#type' => 'fieldset',
390 '#tree' => TRUE,
391 '#title' => t('Administer electoral list'),
392 '#collapsible' => TRUE,
393 '#weight' => 2,
394 '#collapsed' => TRUE,
395 );
396
397 $form['electoral_list']['add_user'] = array(
398 '#type' => 'textfield',
399 '#title' => t('Add user'),
400 '#size' => 40,
401 '#autocomplete_path' => 'user/autocomplete',
402 '#description' => t('Add an individual user to the electoral list'),
403 );
404
405 $form['electoral_list']['submit'] = array(
406 '#type' => 'submit',
407 '#value' => t('Modify electoral list'),
408 );
409
410 $form['electoral_list']['reset'] = array(
411 '#type' => 'button',
412 '#value' => t('Reset electoral list'),
413 );
414
415 $form['nid'] = array('#type' => 'hidden', '#value' => $node->nid);
416 $output .= drupal_get_form('decisions_electoral_list_form', $node->nid);
417 }
418 $output .= t('This table lists all the eligible voters for this Decision.');
419
420 $header[] = array('data' => t('Voter'), 'field' => 'u.name');
421
422 $result = pager_query("SELECT u.uid, u.name FROM {decisions_electoral_list} el LEFT JOIN {users} u ON el.uid = u.uid WHERE el.nid = %d" . tablesort_sql($header), 20, 0, NULL, $node->nid);
423 $eligible_voters = array();
424 while ($voter = db_fetch_object($result)) {
425 $temp = array(theme('username', $voter));
426
427 if (user_access('administer decisions')) {
428 $temp[] = l(t('remove'), 'node/'. $node->nid .'/remove/'. $voter->uid);
429 }
430
431 $eligible_voters[] = $temp;
432 }
433 $output .= theme('table', $header, $eligible_voters);
434 $output .= theme('pager', NULL, 20, 0);
435 print theme('page', $output);
436 }
437 else {
438 drupal_not_found();
439 }
440 }
441
442 /**
443 * Remove an individual voter from the electoral list
444 */
445 function decisions_electoral_list_remove_voter($node, $uid) {
446 # XXX: useless SELECT call
447 $result = db_query('SELECT name FROM {users} WHERE uid=%d', $uid);
448 if ($user = db_fetch_object($result)) {
449 db_query('DELETE FROM {decisions_electoral_list} WHERE nid=%d AND uid=%d', $node->nid, $uid);
450 drupal_set_message(t('%user removed from the electoral list.', array('%user' => $user->name)));
451 }
452 else {
453 drupal_set_message(t('No user found with a uid of %uid.', array('%uid' => $uid)));
454 }
455
456 drupal_goto('node/'. $node->nid .'/electoral_list');
457 }
458
459 /**
460 * Validate changes to the electoral list
461 */
462 function decisions_electoral_list_form_validate($form, &$form_state) {
463 if ($form_state['values']['op'] == t('Reset electoral list')) {
464 if (user_access('administer decisions')) {
465 db_query('DELETE FROM {decisions_electoral_list} WHERE nid=%d', $form_state['values']['nid']);
466 drupal_set_message(t('Electoral list cleared.'));
467 $node = menu_get_object();
468 if (_decisions_electoral_list_reset($node)) {
469 drupal_set_message(t('Electoral list reset.'));
470 }
471 return;
472 }
473 }
474 $add_user = $form_state['values']['electoral_list']['add_user'];
475 if ($add_user) {
476 // Check that the user exists
477 if (!db_fetch_object(db_query('SELECT uid FROM {users} WHERE name="%s"', $add_user))) {
478 form_set_error('electoral_list][add_user', t('User %user does not exist.', array('%user' => $add_user)));
479 return FALSE;
480 }
481 }
482 else {
483 form_set_error('electoral_list][add_user', t('Please enter a user.'));
484 return FALSE;
485 }
486 }
487
488 /**
489 * Submit changes to the electoral list
490 */
491 function decisions_electoral_list_form_submit($form, &$form_state) {
492 $add_user = $form_state['values']['electoral_list']['add_user'];
493 $nid = $form_state['values']['nid'];
494 if ($add_user) {
495 db_query('REPLACE INTO {decisions_electoral_list} (nid, uid) SELECT "%d", u.uid FROM {users} u WHERE u.name = "%s"', $nid, $add_user);
496 drupal_set_message(t('%user added to electoral list.', array('%user' => $add_user)));
497 drupal_goto('node/'. $nid .'/electoral_list');
498 }
499 else {
500 drupal_not_found();
501 }
502 }
503
504
505 /***********************************/
506 /* Decision-mode related functions */
507 /***********************************/
508
509 /**
510 * Show results of the vote.
511 *
512 * This calls the appropriate vote results function, depending on the
513 * mode. It will call the decisions_view_results_$mode hook.
514 */
515 function decisions_view_results(&$node, $teaser, $page) {
516 $mode = _decisions_get_mode($node);
517 $function = "{$mode}_decisions_view_results";
518 if (function_exists($function)) {
519 return call_user_func($function, $node, $teaser, $page);
520 }
521 else {
522 _decisions_panic_on_mode($mode, __FUNCTION__);
523 }
524 }
525
526 /**
527 * View the voting form.
528 *
529 * This calls a function decisions_vote_$mode, where $mode is defined
530 * in the node. If the function does not exist, a watchdog error is
531 * raised and the error is reported using drupal_set_message().
532 *
533 * This also takes care of registering new votes, if the vote button
534 * has been pressed.
535 */
536 function decisions_voting_form($form, &$node, $teaser = FALSE, $page = FALSE) {
537 $mode = _decisions_get_mode($node);
538 if (function_exists("{$mode}_decisions_voting_form")) {
539 return call_user_func("{$mode}_decisions_voting_form", $node, $teaser, $page);
540 }
541 else {
542 _decisions_panic_on_mode($mode, __FUNCTION__);
543 }
544 }
545
546 function decisions_voting_form_submit($form, &$form_state) {
547 $node = node_load($form_state['values']['nid']);
548 decisions_vote($node, $form_state['values']);
549 drupal_set_message(t('Your vote was registered.'));
550 // Transferring makes the results tab display correctly
551 drupal_goto('node/'. $node->nid);
552 }
553
554 /**
555 * Validate vote form submission
556 *
557 * This will call a hook named decisions_vote_validate_$mode and
558 * return its value. hooks should check $POST to see if the vote data
559 * submitted is valid and use form_set_error() if the form has invalid
560 * data.
561 *
562 * @returns boolean true if form has valid data or if no hook is
563 * defined in mode
564 */
565 function decisions_voting_form_validate($form, &$form_state) {
566 $node = node_load($form_state['values']['nid']);
567 $mode = _decisions_get_mode($node);
568 if (function_exists("{$mode}_decisions_vote_validate") ) {
569 return call_user_func("{$mode}_decisions_vote_validate", $node, $form_state['values']);
570 }
571 return TRUE;
572 }
573
574 /**
575 * Record a vote on the node.
576 *
577 * This calls the appropriate vote recording function, depending on
578 * the mode. It will call the decisions_vote_$mode hook.
579 */
580 function decisions_vote($node, $form_values) {
581 $mode = _decisions_get_mode($node);
582 $ok = FALSE; // error by default
583 if (_decisions_eligible($node)) {
584 if (function_exists("{$mode}_decisions_vote")) {
585 call_user_func("{$mode}_decisions_vote", $node, $form_values);
586 }
587 else {
588 _decisions_panic_on_mode($mode, __FUNCTION__);
589 }
590 }
591 else {
592 drupal_set_message(t('You are not eligible to vote on this decision.'));
593 }
594 }
595
596 /**
597 * Helper function to list algorithms for a given mode
598 */
599 function decisions_algorithms($mode) {
600 $algs = array();
601 if (function_exists("{$mode}_decisions_algorithms")) {
602 $algs = call_user_func("{$mode}_decisions_algorithms");
603 $error = FALSE;
604 if (!is_array($algs)) {
605 $error = t('Element returned by the call to function @function is not an array, returning dummy value.',
606 array('@function' => "decisions_{$mode}_algorithms"));
607 }
608 else if (count($algs) == 0) {
609 $error = t('Array returned by the call to function @function is empty, returning dummy value.',
610 array('@function' => "decisions_{$mode}_algorithms"));
611 }
612 if ($error) {
613 watchdog('decisions', $error, WATCHDOG_WARNING);
614 drupal_set_message($error, 'warning');
615 }
616 }
617 else {
618 _decisions_panic_on_mode($mode, __FUNCTION__);
619 }
620 return $algs;
621 }
622
623
624 /*************/
625 /* Callbacks */
626 /*************/
627
628 /**
629 * Callback for canceling a vote.
630 */
631 function decisions_cancel($nid) {
632 if ($node = node_load($nid)) {
633 if ($node->voted && $node->active) {
634 $criteria = votingapi_current_user_identifier();
635 $criteria['content_type'] = 'decisions';
636 $criteria['content_id'] = $node->nid;
637 votingapi_delete_votes(votingapi_select_votes($criteria));
638 drupal_set_message(t('Your vote was canceled.'));
639 }
640 else {
641 drupal_set_message(t("You are not allowed to cancel an invalid choice."), 'error');
642 }
643 drupal_goto('node/'. $nid);
644 }
645 else {
646 drupal_not_found();
647 }
648 }
649
650 /**
651 * Callback to display the votes tab.
652 */
653 function decisions_votes_tab() {
654 if ($node = menu_get_object()) {
655 if (!$node->showvotes) {
656 // Decision is set to not allow viewing of votes
657 drupal_not_found();
658 return;
659 }
660 drupal_set_title(check_plain($node->title));
661 $output = t('This table lists all the recorded votes for this Decision. If anonymous users are allowed to vote, they will be identified by the IP address of the computer they used when they voted.');
662
663 $header[] = array('data' => t('Visitor'), 'field' => 'u.name');
664 $header[] = array('data' => t('Vote'), '');
665
666 /* this query will group all the vote of a user in one record so that the pager can deal with it
667 *
668 * the "votes" column will look something like:
669 *
670 * v1=>t1,v2=>t2
671 *
672 * for a table like this:
673 *
674 * uid | v1 | t1
675 * uid | v2 | t2
676 *
677 * where vN are values and tN are tags (tags being the choice being made and values the score given to it)
678 *
679 * XXX: highly MySQL specific
680 */
681 $query = 'SELECT u.name, v.uid, v.vote_source, GROUP_CONCAT(DISTINCT CONCAT(v.value,"=>",v.tag) ORDER BY v.value) as votes FROM {votingapi_vote} v LEFT JOIN {users} u ON v.uid = u.uid WHERE v.content_id = %d GROUP BY v.uid' . tablesort_sql($header) . '';
682 $query_count = 'SELECT COUNT(DISTINCT v.uid) FROM {votingapi_vote} v LEFT JOIN {users} u ON v.uid = u.uid WHERE v.content_id = %d GROUP BY content_id'. tablesort_sql($header);
683 $result = pager_query($query, variable_get('decisions_votes_per_page', 20), 0, $query_count, $node->nid);
684 $votes = array();
685 $names = array();
686 while ($vote = db_fetch_object($result)) {
687 $key = $vote->uid? $vote->uid : $vote->vote_source;
688 $choices = explode(',', $vote->votes);
689 foreach ($choices as $choice) {
690 $choice = explode("=>", $choice);
691 $votes[$key][] = (object) array('value' => $choice[0], 'tag' => $choice[1]);
692 }
693 $names[$key] = $vote->name ? theme('username', $vote) : check_plain($vote->vote_source);
694 }
695
696 $mode = _decisions_get_mode($node);
697 $function_format_votes = "{$mode}_decisions_format_votes";
698 if (!function_exists($function_format_votes)) {
699 _decisions_panic_on_mode($mode, __FUNCTION__);
700 drupal_not_found();
701 }
702
703 $rows = array();
704 foreach ($names as $key => $name) {
705 $rows[$key]['name'] = $name;
706 $rows[$key]['vote'] = call_user_func($function_format_votes, $node, $votes[$key]);
707 }
708
709 $output .= theme('table', $header, $rows);
710 $output .= theme('pager', array(), variable_get('decisions_votes_per_page', 20), 0);
711 print theme('page', $output);
712 }
713 else {
714 drupal_not_found();
715 }
716 }
717
718 /**
719 * Callback for 'results' tab for decisions you can vote on.
720 */
721 function decisions_results() {
722 if ($node = menu_get_object()) {
723 drupal_set_title(check_plain($node->title));
724 return node_show($node, 0);
725 }
726 else {
727 // The url does not provide the appropriate node id
728 drupal_not_found();
729 }
730 }
731
732 /**
733 * Callback to display a reset votes confirmation form
734 */
735 function decisions_reset_form($form_state, $node) {
736 $form['nid'] = array('#type' => 'hidden', '#value' => $node->nid);
737 return confirm_form($form,
738 t('Are you sure you want to reset the votes for !title?',
739 array('!title' => theme('placeholder', $node->title))),
740 'node/'. $node->nid,
741 t('This action cannot be undone.'),
742 t('Reset votes'),
743 t('Cancel') );
744 }
745
746 /**
747 * Reset votes once the confirmation is given
748 */
749 function decisions_reset_form_submit($form, &$form_state) {
750 $nid = $form_state['values']['nid'];
751 // Delete any votes for the poll
752 db_query("DELETE FROM {votingapi_vote} WHERE content_id = %d", $nid);
753 drupal_set_message('Votes have been reset.');
754 drupal_goto('node/'. $nid);
755 }
756
757 /**
758 * Return the mode of a decision based on its type
759 */
760 function _decisions_get_mode($node) {
761 if ($node->type) {
762 $types = explode('_', $node->type, 2);
763 return $types[1];
764 }
765 else {
766 drupal_set_message('No type specified for node: '. $node->nid, 'error');
767 return '';
768 }
769 }
770
771 /**
772 * Callback function to see if a node is acceptable for poll menu items.
773 */
774 function _decisions_votes_access($node, $perm) {
775 return user_access($perm) && $node->showvotes && strpos($node->type, 'decisions_') === 0;
776 }
777
778 function _decisions_reset_access($node, $perm) {
779 return user_access($perm) && strpos($node->type, 'decisions_') === 0;
780 }
781
782 function _decisions_electoral_list_access($node, $perm) {
783 return user_access($perm) && $node->uselist;
784 }
785
786 /**
787 * Function that tells if the given decision is open to votes.
788 */
789 function _decisions_is_open($node) {
790 $time = time();
791 return ($node->active && // node must be active
792 // current time must be past start date and before end date
793 ($time >= $node->startdate) &&
794 ($node->runtime == DECISIONS_RUNTIME_INFINITY ||
795 $time < ($node->startdate + $node->runtime)));
796 }
797
798 /**
799 * Function that tells if the given user can vote on this decision.
800 */
801 function _decisions_can_vote($node, $user = NULL) {
802 return (_decisions_is_open($node) && // node must be open
803 !$node->voted && // user should not have already voted
804 _decisions_eligible($node, $user)); // user must be eligible to vote
805 }
806
807 /**
808 * Function that tells if the given decision meets the quorum.
809 */
810 function _decisions_meets_quorum($node) {
811 // compute number of people that have cast their vote
812 $num_voters = _decisions_count_voters($node);
813 $quorum = _decisions_get_quorum($node);
814 return ($num_voters >= $quorum);
815 }
816
817
818 /**
819 * Internal function factored out that just rings lots of bells when
820 * we detect an unknown mode.
821 */
822 function _decisions_panic_on_mode($mode, $function = '') {
823 watchdog('decisions', 'Unknown decision mode : @mode in "@function".', array('@mode' => $mode, '@function' => $function, WATCHDOG_ERROR));
824 drupal_set_message(t('Unknown decision mode : @mode in "@function".', array('@mode' => $mode, '@function' => $function), 'error'));
825 }
826
827 /**
828 * Get all votes from the given node.
829 */
830 function _decisions_votes($node) {
831 $votes = array();
832 // we bypass votingapi because we need ORDER BY value ASC lets us ensure no gaps
833 $result = db_query("SELECT * FROM {votingapi_vote} v WHERE content_type='%s' AND content_id='%d' ORDER BY value ASC", 'decisions', $node->nid);
834 while ($vobj = db_fetch_array($result)) {
835 $votes[] = $vobj;
836 }
837 return $votes;
838 }
839
840 /**
841 * Count the elligible voters for a given decision.
842 */
843 function _decisions_count_eligible($node) {
844 if ($node->uselist) {
845 $result = db_fetch_object(db_query("SELECT COUNT(*) AS num FROM {decisions_electoral_list} WHERE nid=%d", $node->nid));
846 }
847 else {
848 // check first if authenticated users have the right to vote, because
849 // authenticated users are not added to the users_roles permission,
850 // probably for performance reasons
851 $roles = user_roles(FALSE, 'vote on decisions');
852 if ($roles[DRUPAL_AUTHENTICATED_RID]) {
853 // special case: any authenticated user can vote
854 // consider all current to be elligible
855 $result = db_fetch_object(db_query("SELECT COUNT(*) AS num FROM {users} u WHERE u.uid <> 0"));
856 }
857 else {
858 // only some roles are elligible, add relevant users only
859 $result = db_fetch_object(db_query("SELECT COUNT(DISTINCT ur.uid) AS num FROM {users_roles} ur JOIN {permission} p ON ur.rid = p.rid WHERE FIND_IN_SET(' vote on decisions', p.perm) AND ur.uid <> 0"));
860 }
861 }
862 return $result->num;
863 }
864
865 /**
866 * Returns the quorum (minimum voters) of a node.
867 */
868 function _decisions_get_quorum($node) {
869 $num_eligible_voters = _decisions_count_eligible($node);
870 $quorum = $node->quorum_abs + ceil(($node->quorum_percent / 100.0) * $num_eligible_voters);
871 return min($quorum, $num_eligible_voters);
872 }
873
874 /**
875 * Count the number of distinct voters.
876 */
877 function _decisions_count_voters($node) {
878 $num_voters = 0;
879 if ($result = db_fetch_object(db_query("SELECT COUNT(DISTINCT CONCAT(uid,vote_source)) AS voters FROM {votingapi_vote} WHERE content_id=%d", $node->nid))) {
880 $num_voters = $result->voters;
881 }
882 return $num_voters;
883 }
884
885 /**
886 * Get all votes by uid in a an array, in a uid => votes fashion.
887 */
888 function _decisions_user_votes($node) {
889 $votes = _decisions_votes($node);
890
891 // aggregate votes by user (uid if logged in, IP if anonymous)
892 // in ascending order of value
893 $user_votes = array();
894
895 foreach ($votes as $vote) {
896 $key = ($vote->uid == 0 ? $vote->vote_source: $vote->uid);
897 $user_votes[$key][] = $vote;
898 }
899
900 return $user_votes;
901 }
902
903 /**
904 * Check if user is eligible to this decision.
905 */
906 function _decisions_eligible($node, $uid = NULL) {
907 global $user;
908 if (is_null($uid)) {
909 $uid = $user->uid;
910 }
911
912 if ($node->uselist) {
913 $can_vote = db_fetch_object(db_query("SELECT COUNT(*) AS eligible FROM {decisions_electoral_list} WHERE nid=%d AND uid=%d", $node->nid, $uid));
914 $eligible = $can_vote->eligible;
915 }
916 else {
917 $eligible = user_access('vote on decisions');
918 }
919 return $eligible;
920 }
921
922 /**
923 * Constructs the time select boxes.
924 *
925 * @ingroup event_support
926 * @param $timestamp The time GMT timestamp of the event to use as the default
927 * value.
928 * @return An array of form elements for month, day, year, hour, and minute
929 */
930 function _decisions_form_date($timestamp) {
931 // populate drop down values...
932 // ...months
933 $months = array(1 => t('January'), t('February'), t('March'), t('April'), t('May'), t('June'), t('July'), t('August'), t('September'), t('October'), t('November'), t('December'));
934 // ...hours
935 if (variable_get('event_ampm', '0')) {
936 $hour_format = t('g');
937 $hours = drupal_map_assoc(range(1, 12));
938 $am_pms = array('am' => t('am'), 'pm' => t('pm'));
939 }
940 else {
941 $hour_format = t('H');
942 $hours = drupal_map_assoc(range(0, 23));
943 }
944 // ...minutes (with leading 0s)
945 for ($i = 0; $i <= 59; $i++) $minutes[$i] = $i < 10 ? "0$i" : $i;
946
947 // This is a GMT timestamp, so the _event_date() wrapper to display local times.
948 $form['day'] = array(
949 '#prefix' => '<div class="container-inline"><div class="day">',
950 '#type' => 'textfield',
951 '#default_value' => _decisions_date('d', $timestamp),
952 '#maxlength' => 2,
953 '#size' => 2,
954 '#required' => TRUE);
955 $form['month'] = array(
956 '#type' => 'select',
957 '#default_value' => _decisions_date('n', $timestamp),
958 '#options' => $months,
959 '#required' => TRUE);
960 $form['year'] = array(
961 '#type' => 'textfield',
962 '#default_value' => _decisions_date('Y', $timestamp),
963 '#maxlength' => 4,
964 '#size' => 4,
965 '#required' => TRUE);
966 $form['hour'] = array(
967 '#prefix' => '</div>&#8212;<div class="time">',
968 '#type' => 'select',
969 '#default_value' => _decisions_date($hour_format, $timestamp),
970 '#options' => $hours,
971 '#required' => TRUE);
972 $form['minute'] = array(
973 '#prefix' => ':',
974 '#type' => 'select',
975 '#default_value' => _decisions_date('i', $timestamp),
976 '#options' => $minutes,
977 '#required' => TRUE);
978 if (isset($am_pms)) {
979 $form['ampm'] = array(
980 '#type' => 'radios',
981 '#default_value' => _decisions_date('a', $timestamp),
982 '#options' => $am_pms,
983 '#required' => TRUE);
984 }
985 $form['close'] = array(
986 '#type' => 'markup',
987 '#value' => '</div></div>');
988
989 return $form;
990 }
991
992 /**
993 * Takes a time element and prepares to send it to form_date()
994 *
995 * @param $time
996 * The time to be turned into an array. This can be:
997 * - a timestamp when from the database
998 * - an array (day, month, year) when previewing
999 * - null for new nodes
1000 * @returnn
1001 * an array for form_date (day, month, year)
1002 */
1003 function _decisions_form_prepare_datetime($time = '', $offset = 0) {
1004 // if this is empty, get the current time
1005 if ($time == '') {
1006 $time = time();
1007 $time = strtotime("+$offset days", $time);
1008 }
1009 // If we are previewing, $time will be an array so just pass it through
1010 $time_array = array();
1011 if (is_array($time)) {
1012 $time_array = $time;
1013 }
1014 // otherwise build the array from the timestamp
1015 elseif (is_numeric($time)) {
1016 $time_array = array(
1017 'day' => _decisions_date('j', $time),
1018 'month' => _decisions_date('n', $time),
1019 'year' => _decisions_date('Y', $time),
1020 'hour' => _decisions_date('H', $time),
1021 'min' => _decisions_date('i', $time),
1022 'sec' => _decisions_date('s', $time),
1023 );
1024 }
1025 // return the array
1026 return $time_array;
1027 }
1028
1029 /**
1030 * Content of the block, as returned by decisions_block('view')
1031 */
1032 function _decisions_block_mostrecent() {
1033 $output = '';
1034 $result = db_query_range('SELECT nid FROM {decisions} WHERE active=1 ORDER BY nid DESC', 1);
1035 // Check that there is an active decision
1036 if ($decision = db_fetch_object($result)) {
1037 $n = decisions_view(node_load($decision->nid), FALSE, FALSE, TRUE);
1038 /* XXX: we have to do this because somehow the #printed settings lives across multiple node_load */
1039 unset($n->content['#printed']);
1040 $output = drupal_render($n->content);
1041 }
1042 else {
1043 $output = t('No active decisions.');
1044 }
1045 return $output;
1046 }
1047
1048 /**
1049 * Returns true if the user can view the results of current node.
1050 */
1051 function _decisions_can_view_results($node) {
1052 $view_results = variable_get('decisions_view_results', DECISIONS_DEFAULT_VIEW_RESULTS);
1053 return (_decisions_meets_quorum($node) && // node meets the quorum
1054 strpos($node->type, 'decisions_') === 0 &&
1055 (!_decisions_is_open($node) // node is closed
1056 || ($node->voted && $view_results == 'aftervote') // user voted
1057 || ($view_results == 'always'))); // all can view
1058 }
1059
1060 /**
1061 * Insert the right users in the electoral list
1062 */
1063 function _decisions_electoral_list_reset($node) {
1064 // check first if authenticated users have the right to vote, because authenticated users are not added to the users_roles permission, probably for performance reasons
1065 $result = db_fetch_object(db_query("SELECT COUNT(*) AS hit FROM {permission} JOIN role ON role.rid = permission.rid WHERE FIND_IN_SET(' vote on decisions', perm) AND role.name = 'authenticated user'"));
1066 if (isset($result) && $result->hit) {
1067 // special case: any authenticated user can vote
1068 // add all current users to electoral list
1069 return db_query("INSERT INTO {decisions_electoral_list} (nid, uid) SELECT '%d', u.uid FROM users u WHERE u.uid <> 0", $node->nid);
1070 }
1071 else {
1072 // all users must not be allowed to vote, add relevant users only
1073 return db_query("INSERT INTO {decisions_electoral_list} (nid, uid) SELECT '%d', u.uid FROM users_roles u, permission p WHERE FIND_IN_SET(' view decisions', p.perm) AND u.rid = p.rid AND u.uid <> 0", $node->nid);
1074 }
1075
1076 }
1077
1078 /**
1079 * Implementation of hook_load().
1080 *
1081 * Load the votes and decision-specific data into the node object.
1082 */
1083 function decisions_load($node) {
1084 $decision = db_fetch_object(db_query("SELECT * FROM {decisions} WHERE nid = %d", $node->nid));
1085 $result = db_query("SELECT vote_offset, label FROM {decisions_choices} WHERE nid = %d ORDER BY vote_offset", $node->nid);
1086 while ($choice = db_fetch_array($result)) {
1087 $decision->choice[$choice['vote_offset']] = $choice;
1088 }
1089 $decision->choices = count($decision->choice);
1090
1091 // See if user has voted
1092 $criteria = votingapi_current_user_identifier();
1093 $criteria['content_type'] = 'decisions';
1094 $criteria['content_id'] = $node->nid;
1095 $decision->voted = count(votingapi_select_votes($criteria)) > 0;
1096
1097 return $decision;
1098 }
1099
1100 /**
1101 * Implementation of hook_delete().
1102 *
1103 */
1104 function decisions_delete($node) {
1105 db_query("DELETE FROM {decisions} WHERE nid = %d", $node->nid);
1106 db_query("DELETE FROM {decisions_choices} WHERE nid = %d", $node->nid);
1107 db_query("DELETE FROM {decisions_electoral_list} WHERE nid = %d", $node->nid);
1108
1109 // Note: this should be converted to a votingapi method eventually
1110 db_query("DELETE FROM {votingapi_vote} WHERE content_id = %d", $node->nid);
1111 }
1112
1113 /**
1114 * Implementation of hook_insert()
1115 *
1116 * This is called upon node creation
1117 */
1118 function decisions_insert($node) {
1119 // Compute startdate and runtime.
1120 $startdate = _decisions_translate_form_date($node->settings['date']['startdate']['date']);
1121 if ($node->settings['date']['noenddate']) {
1122 $runtime = DECISIONS_RUNTIME_INFINITY;
1123 }
1124 else {
1125 $enddate = _decisions_translate_form_date($node->settings['date']['enddate']['date']);
1126 if ($enddate < $startdate) {
1127 form_set_error('enddate', t('The specified close date is less than the opening date, setting it to the same for now.'));
1128 $enddate = $startdate;
1129 }
1130 $runtime = $enddate - $startdate;
1131 }
1132
1133 // just create an empty entry for now
1134 $mode = _decisions_get_mode($node);
1135
1136 db_query("INSERT INTO {decisions} (nid, mode, quorum_abs, quorum_percent, uselist, active, runtime, maxchoices, algorithm, startdate, randomize) VALUES (%d, '%s', %d, %f, %d, %d, %d, %d, '%s', %d, %d)", $node->nid, $mode, $node->settings['quorum']['quorum_abs'], $node->settings['quorum']['quorum_percent'], $node->settings['uselist'], $node->settings['active'], $node->settings['runtime'], $node->settings['maxchoices'], $node->settings['algorithm'], $startdate, $node->settings['randomize']);
1137
1138 // create the electoral list if desired
1139
1140 if ($node->settings['uselist']) {
1141 _decisions_electoral_list_reset($node);
1142 }
1143
1144 // insert the choices, same sequence than update
1145 decisions_update($node);
1146 }
1147
1148 /**
1149 * Implementation of hook_validate().
1150 *
1151 * XXX: No validation yet.
1152 */
1153 function decisions_validate(&$node) {
1154 // Use form_set_error for any errors
1155 $node->choice = array_values($node->choice);
1156
1157 // Start keys at 1 rather than 0
1158 array_unshift($node->choice, '');
1159 unset($node->choice[0]);
1160
1161 // Check for at least two choices
1162 $realchoices = 0;
1163 foreach ($node->choice as $i => $choice) {
1164 if ($choice['label'] != '') {
1165 $realchoices++;
1166 }
1167 }
1168
1169 if ($realchoices < 2) {
1170 form_set_error("choice][$realchoices][label", t('You must fill in at least two choices.'));
1171 }
1172
1173 $startdate